Skip to content

Commit f0f9575

Browse files
committed
feat(zipatch): support virtual filesystem
1 parent f6f9b56 commit f0f9575

4 files changed

Lines changed: 126 additions & 14 deletions

File tree

packages/zipatch/src/chunks/sqpk.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { inflateRawSync } from 'node:zlib'
22
import $debug from 'debug'
33
import { SmartBuffer } from 'smart-buffer'
4-
import type { FileSystem } from '../fs'
4+
import type { ZipatchFileSystem } from '../fs'
55
import type { ZipatchChunkHandler } from '../interface'
66
import {
77
type FileHeader,
@@ -27,7 +27,7 @@ const debug = $debug('zipatch:sqpk')
2727
* Handles SqPack-based game install operations
2828
*/
2929

30-
type SqpkHandler = (buffer: SmartBuffer, fs: FileSystem) => Promise<void>
30+
type SqpkHandler = (buffer: SmartBuffer, fs: ZipatchFileSystem) => Promise<void>
3131

3232
const eraseHandler: SqpkHandler = async (buffer, fs) => {
3333
const payload = readSqpkDataHeader(buffer)
@@ -55,10 +55,9 @@ const blockHeaderSize = 16
5555
const addFileHandler = async (
5656
fileHeader: FileHeader,
5757
buffer: SmartBuffer,
58-
fs: FileSystem,
58+
fs: ZipatchFileSystem,
5959
) => {
60-
const handle = await fs.getFileHandle(fileHeader.filePath)
61-
if (!handle) {
60+
if (!fs.isPathAllowed(fileHeader.filePath)) {
6261
return
6362
}
6463

@@ -95,10 +94,10 @@ const addFileHandler = async (
9594
const data = buffer.readBuffer(blockHeader.blockSize)
9695
if (blockHeader.isBlockCompressed) {
9796
const inflated = inflateRawSync(data)
98-
await handle.write(inflated, 0, inflated.length, offset)
97+
await fs.write(fileHeader.filePath, inflated, offset)
9998
bytesWritten += inflated.length
10099
} else {
101-
await handle.write(data, 0, data.length, offset)
100+
await fs.write(fileHeader.filePath, data, offset)
102101
bytesWritten += data.length
103102
}
104103

packages/zipatch/src/fs.ts

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,33 @@ import $debug from 'debug'
66
const debug = $debug('zipatch:fs')
77

88
const zeroBuffer = Buffer.alloc(1024, 0)
9-
export class FileSystem {
9+
export interface FileRange {
10+
start: number
11+
end: number
12+
}
13+
14+
export interface ZipatchFileSystem {
15+
workspace: string
16+
close(): Promise<void>
17+
isPathAllowed(path: string): boolean
18+
write(path: string, buf: Buffer, offset?: number): Promise<boolean>
19+
erase(path: string, length: number, offset: number): Promise<boolean>
20+
createDirectory(path: string): Promise<void>
21+
removeDirectory(path: string): Promise<void>
22+
}
23+
24+
export class FileSystem implements ZipatchFileSystem {
1025
private fileHandles = new Map<string, FileHandle>()
1126

1227
constructor(
1328
private root: string,
1429
private allowList: string[] = [],
1530
) {}
1631

32+
get workspace(): string {
33+
return this.root
34+
}
35+
1736
/**
1837
* Close all open file handles
1938
*/
@@ -35,7 +54,14 @@ export class FileSystem {
3554
* Check if a path is allowed based on the allowlist
3655
*/
3756
isPathAllowed(path: string): boolean {
38-
return !this.allowList.length || this.allowList.includes(path)
57+
return (
58+
!this.allowList.length ||
59+
this.allowList.some((pattern) =>
60+
pattern.endsWith('*')
61+
? path.startsWith(pattern.slice(0, -1))
62+
: pattern === path,
63+
)
64+
)
3965
}
4066

4167
/**
@@ -128,3 +154,81 @@ export class FileSystem {
128154
return handle
129155
}
130156
}
157+
158+
export class VirtualFileSystem implements ZipatchFileSystem {
159+
private ranges = new Map<string, FileRange[]>()
160+
161+
constructor(
162+
private root: string,
163+
private allowList: string[] = [],
164+
) {}
165+
166+
get workspace(): string {
167+
return this.root
168+
}
169+
170+
async close(): Promise<void> {
171+
// do nothing
172+
}
173+
174+
isPathAllowed(path: string): boolean {
175+
return (
176+
!this.allowList.length ||
177+
this.allowList.some((pattern) =>
178+
pattern.endsWith('*')
179+
? path.startsWith(pattern.slice(0, -1))
180+
: pattern === path,
181+
)
182+
)
183+
}
184+
185+
async write(path: string, buf: Buffer, offset: number = 0): Promise<boolean> {
186+
this.recordRange(path, offset, offset + buf.length)
187+
return true
188+
}
189+
190+
async erase(path: string, length: number, offset: number): Promise<boolean> {
191+
this.recordRange(path, offset, offset + length)
192+
return true
193+
}
194+
195+
async createDirectory(path: string): Promise<void> {
196+
// do nothing
197+
}
198+
199+
async removeDirectory(path: string): Promise<void> {
200+
// do nothing
201+
}
202+
203+
getRecordedRanges(): Map<string, FileRange[]> {
204+
return new Map(
205+
[...this.ranges.entries()].map(([path, ranges]) => [
206+
path,
207+
ranges.map((range) => ({ ...range })),
208+
]),
209+
)
210+
}
211+
212+
private recordRange(path: string, start: number, end: number) {
213+
if (!this.isPathAllowed(path)) {
214+
return
215+
}
216+
217+
const ranges = this.ranges.get(path) ?? []
218+
ranges.push({ start, end })
219+
ranges.sort((left, right) => left.start - right.start)
220+
221+
const merged: FileRange[] = []
222+
for (const range of ranges) {
223+
const current = merged.at(-1)
224+
if (!current || range.start > current.end) {
225+
merged.push({ ...range })
226+
continue
227+
}
228+
229+
current.end = Math.max(current.end, range.end)
230+
}
231+
232+
this.ranges.set(path, merged)
233+
}
234+
}

packages/zipatch/src/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
type ZipatchPreloadData,
1010
type ZipatchPreloadedChunk,
1111
} from './datasource'
12-
import { FileSystem } from './fs'
12+
import { FileSystem, VirtualFileSystem, type ZipatchFileSystem } from './fs'
1313
import type { ZipatchChunk, ZipatchContext } from './interface'
1414

1515
const zipatchMagic = [
@@ -25,6 +25,12 @@ export type {
2525
ZipatchPreloadData,
2626
ZipatchPreloadedChunk,
2727
} from './datasource'
28+
export {
29+
type FileRange,
30+
FileSystem,
31+
VirtualFileSystem,
32+
type ZipatchFileSystem,
33+
} from './fs'
2834

2935
export class ZipatchReader {
3036
private source!: ZipatchDataSource
@@ -127,12 +133,15 @@ export class ZipatchReader {
127133
* Extract the zipatch file to a directory.
128134
*/
129135
async applyTo(path: string, allowList: string[] = []) {
130-
// Ensure the extraction directory exists
131136
const fs = new FileSystem(path, allowList)
137+
await this.apply(fs, allowList)
138+
}
139+
140+
async apply(fs: ZipatchFileSystem, allowList: string[] = []) {
132141
await fs.createDirectory('/')
133142
const context: ZipatchContext = {
134143
platform: 'win32',
135-
workspace: path,
144+
workspace: fs.workspace,
136145
allowList,
137146
fs,
138147
}

packages/zipatch/src/interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { FileSystem } from './fs'
1+
import type { ZipatchFileSystem } from './fs'
22

33
export interface ZipatchContext {
44
platform: 'win32'
55
workspace: string
66
allowList: string[]
7-
fs: FileSystem
7+
fs: ZipatchFileSystem
88
}
99

1010
export interface ZipatchChunk {

0 commit comments

Comments
 (0)