@componentor/fs 3.0.17 → 3.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.js +46 -3
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +38 -1
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +38 -1
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +109 -3
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -154,6 +154,13 @@ var VFSEngine = class {
|
|
|
154
154
|
superblockDirty = false;
|
|
155
155
|
// Free inode hint — skip O(n) scan
|
|
156
156
|
freeInodeHint = 0;
|
|
157
|
+
// Configurable upper bounds
|
|
158
|
+
maxInodes = 4e6;
|
|
159
|
+
maxBlocks = 4e6;
|
|
160
|
+
maxPathTable = 256 * 1024 * 1024;
|
|
161
|
+
// 256MB
|
|
162
|
+
maxVFSSize = 100 * 1024 * 1024 * 1024;
|
|
163
|
+
// 100GB
|
|
157
164
|
init(handle, opts) {
|
|
158
165
|
this.handle = handle;
|
|
159
166
|
this.processUid = opts?.uid ?? 0;
|
|
@@ -161,11 +168,23 @@ var VFSEngine = class {
|
|
|
161
168
|
this.umask = opts?.umask ?? DEFAULT_UMASK;
|
|
162
169
|
this.strictPermissions = opts?.strictPermissions ?? false;
|
|
163
170
|
this.debug = opts?.debug ?? false;
|
|
171
|
+
if (opts?.limits) {
|
|
172
|
+
if (opts.limits.maxInodes != null) this.maxInodes = opts.limits.maxInodes;
|
|
173
|
+
if (opts.limits.maxBlocks != null) this.maxBlocks = opts.limits.maxBlocks;
|
|
174
|
+
if (opts.limits.maxPathTable != null) this.maxPathTable = opts.limits.maxPathTable;
|
|
175
|
+
if (opts.limits.maxVFSSize != null) this.maxVFSSize = opts.limits.maxVFSSize;
|
|
176
|
+
}
|
|
164
177
|
const size = handle.getSize();
|
|
165
178
|
if (size === 0) {
|
|
166
179
|
this.format();
|
|
167
180
|
} else {
|
|
168
|
-
|
|
181
|
+
try {
|
|
182
|
+
this.mount();
|
|
183
|
+
} catch (err) {
|
|
184
|
+
const msg = err.message ?? String(err);
|
|
185
|
+
if (msg.startsWith("Corrupt VFS:")) throw err;
|
|
186
|
+
throw new Error(`Corrupt VFS: ${msg}`);
|
|
187
|
+
}
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
190
|
/** Release the sync access handle (call on fatal error or shutdown) */
|
|
@@ -232,6 +251,18 @@ var VFSEngine = class {
|
|
|
232
251
|
if (freeBlocks > totalBlocks) {
|
|
233
252
|
throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
|
|
234
253
|
}
|
|
254
|
+
if (inodeCount > this.maxInodes) {
|
|
255
|
+
throw new Error(`Corrupt VFS: inode count ${inodeCount} exceeds maximum ${this.maxInodes}`);
|
|
256
|
+
}
|
|
257
|
+
if (totalBlocks > this.maxBlocks) {
|
|
258
|
+
throw new Error(`Corrupt VFS: total blocks ${totalBlocks} exceeds maximum ${this.maxBlocks}`);
|
|
259
|
+
}
|
|
260
|
+
if (fileSize > this.maxVFSSize) {
|
|
261
|
+
throw new Error(`Corrupt VFS: file size ${fileSize} exceeds maximum ${this.maxVFSSize}`);
|
|
262
|
+
}
|
|
263
|
+
if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) {
|
|
264
|
+
throw new Error(`Corrupt VFS: non-finite or negative section offset`);
|
|
265
|
+
}
|
|
235
266
|
if (inodeTableOffset !== SUPERBLOCK.SIZE) {
|
|
236
267
|
throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
|
|
237
268
|
}
|
|
@@ -249,7 +280,13 @@ var VFSEngine = class {
|
|
|
249
280
|
if (pathUsed > pathTableSize) {
|
|
250
281
|
throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
|
|
251
282
|
}
|
|
283
|
+
if (pathTableSize > this.maxPathTable) {
|
|
284
|
+
throw new Error(`Corrupt VFS: path table size ${pathTableSize} exceeds maximum ${this.maxPathTable}`);
|
|
285
|
+
}
|
|
252
286
|
const expectedMinSize = dataOffset + totalBlocks * blockSize;
|
|
287
|
+
if (expectedMinSize > this.maxVFSSize) {
|
|
288
|
+
throw new Error(`Corrupt VFS: computed layout size ${expectedMinSize} exceeds maximum ${this.maxVFSSize}`);
|
|
289
|
+
}
|
|
253
290
|
if (fileSize < expectedMinSize) {
|
|
254
291
|
throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
|
|
255
292
|
}
|
|
@@ -2557,6 +2594,10 @@ function readPayload(targetSab, targetCtrl) {
|
|
|
2557
2594
|
if (totalLen <= maxChunk) {
|
|
2558
2595
|
return new Uint8Array(targetSab, HEADER_SIZE, chunkLen).slice();
|
|
2559
2596
|
}
|
|
2597
|
+
if (totalLen > activeLimits.maxPayload || totalLen <= 0) {
|
|
2598
|
+
console.error(`[sync-relay] readPayload: totalLen=${totalLen} exceeds limit (${activeLimits.maxPayload}) or invalid`);
|
|
2599
|
+
return new Uint8Array(0);
|
|
2600
|
+
}
|
|
2560
2601
|
const fullBuffer = new Uint8Array(totalLen);
|
|
2561
2602
|
let offset = 0;
|
|
2562
2603
|
fullBuffer.set(new Uint8Array(targetSab, HEADER_SIZE, chunkLen), offset);
|
|
@@ -2566,6 +2607,10 @@ function readPayload(targetSab, targetCtrl) {
|
|
|
2566
2607
|
Atomics.notify(targetCtrl, 0);
|
|
2567
2608
|
Atomics.wait(targetCtrl, 0, SIGNAL.CHUNK_ACK);
|
|
2568
2609
|
const nextLen = Atomics.load(targetCtrl, 3);
|
|
2610
|
+
if (nextLen <= 0 || nextLen > maxChunk) {
|
|
2611
|
+
console.error(`[sync-relay] readPayload: invalid nextLen=${nextLen} at offset=${offset}`);
|
|
2612
|
+
return fullBuffer.slice(0, offset);
|
|
2613
|
+
}
|
|
2569
2614
|
fullBuffer.set(new Uint8Array(targetSab, HEADER_SIZE, nextLen), offset);
|
|
2570
2615
|
offset += nextLen;
|
|
2571
2616
|
}
|
|
@@ -2770,8 +2815,57 @@ async function scanOPFSEntries(dir, prefix) {
|
|
|
2770
2815
|
}
|
|
2771
2816
|
return result;
|
|
2772
2817
|
}
|
|
2818
|
+
var DEFAULT_LIMITS = {
|
|
2819
|
+
maxInodes: 4e6,
|
|
2820
|
+
maxBlocks: 4e6,
|
|
2821
|
+
maxPathTable: 256 * 1024 * 1024,
|
|
2822
|
+
maxVFSSize: 100 * 1024 * 1024 * 1024,
|
|
2823
|
+
maxPayload: 2 * 1024 * 1024 * 1024
|
|
2824
|
+
};
|
|
2825
|
+
function resolveLimits(input) {
|
|
2826
|
+
return { ...DEFAULT_LIMITS, ...input };
|
|
2827
|
+
}
|
|
2828
|
+
var activeLimits = { ...DEFAULT_LIMITS };
|
|
2829
|
+
function quickValidateVFS(handle, fileSize, limits) {
|
|
2830
|
+
if (fileSize < SUPERBLOCK.SIZE) return `file too small (${fileSize} bytes)`;
|
|
2831
|
+
const buf = new Uint8Array(SUPERBLOCK.SIZE);
|
|
2832
|
+
handle.read(buf, { at: 0 });
|
|
2833
|
+
const v = new DataView(buf.buffer);
|
|
2834
|
+
const magic = v.getUint32(SUPERBLOCK.MAGIC, true);
|
|
2835
|
+
if (magic !== VFS_MAGIC) return `bad magic 0x${magic.toString(16)}`;
|
|
2836
|
+
const version = v.getUint32(SUPERBLOCK.VERSION, true);
|
|
2837
|
+
if (version !== VFS_VERSION) return `unsupported version ${version}`;
|
|
2838
|
+
const inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
|
|
2839
|
+
const blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
|
|
2840
|
+
const totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
|
|
2841
|
+
const freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
|
|
2842
|
+
const inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
|
|
2843
|
+
const pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
|
|
2844
|
+
const dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
|
|
2845
|
+
const bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
|
|
2846
|
+
const pathUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
|
|
2847
|
+
if (blockSize === 0 || (blockSize & blockSize - 1) !== 0) return `invalid block size ${blockSize}`;
|
|
2848
|
+
if (inodeCount === 0) return "inode count is 0";
|
|
2849
|
+
if (inodeCount > limits.maxInodes) return `inode count ${inodeCount} exceeds maximum ${limits.maxInodes}`;
|
|
2850
|
+
if (totalBlocks > limits.maxBlocks) return `total blocks ${totalBlocks} exceeds maximum ${limits.maxBlocks}`;
|
|
2851
|
+
if (freeBlocks > totalBlocks) return `free blocks (${freeBlocks}) exceeds total (${totalBlocks})`;
|
|
2852
|
+
if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) return "non-finite or negative section offset";
|
|
2853
|
+
if (inodeTableOffset !== SUPERBLOCK.SIZE) return `inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`;
|
|
2854
|
+
const expectedPathOffset = inodeTableOffset + inodeCount * INODE_SIZE;
|
|
2855
|
+
if (pathTableOffset !== expectedPathOffset) return `path table offset ${pathTableOffset} (expected ${expectedPathOffset})`;
|
|
2856
|
+
if (bitmapOffset <= pathTableOffset) return "bitmap offset must be after path table";
|
|
2857
|
+
if (dataOffset <= bitmapOffset) return "data offset must be after bitmap";
|
|
2858
|
+
const pathTableSize = bitmapOffset - pathTableOffset;
|
|
2859
|
+
if (pathUsed > pathTableSize) return `path used (${pathUsed}) exceeds path table size (${pathTableSize})`;
|
|
2860
|
+
if (pathTableSize > limits.maxPathTable) return `path table size ${pathTableSize} exceeds maximum ${limits.maxPathTable}`;
|
|
2861
|
+
const expectedMinSize = dataOffset + totalBlocks * blockSize;
|
|
2862
|
+
if (expectedMinSize > limits.maxVFSSize) return `computed layout size ${expectedMinSize} exceeds maximum ${limits.maxVFSSize}`;
|
|
2863
|
+
if (fileSize < expectedMinSize) return `file size ${fileSize} too small for layout (need ${expectedMinSize})`;
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2773
2866
|
async function initEngine(config) {
|
|
2774
2867
|
debug = config.debug ?? false;
|
|
2868
|
+
activeLimits = resolveLimits(config.limits);
|
|
2775
2869
|
let rootDir = await navigator.storage.getDirectory();
|
|
2776
2870
|
if (config.root && config.root !== "/") {
|
|
2777
2871
|
const segments = config.root.split("/").filter(Boolean);
|
|
@@ -2781,14 +2875,26 @@ async function initEngine(config) {
|
|
|
2781
2875
|
}
|
|
2782
2876
|
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
2783
2877
|
const vfsHandle = await vfsFileHandle.createSyncAccessHandle();
|
|
2784
|
-
const
|
|
2878
|
+
const vfsSize = vfsHandle.getSize();
|
|
2879
|
+
if (vfsSize > 0) {
|
|
2880
|
+
const validationError = quickValidateVFS(vfsHandle, vfsSize, activeLimits);
|
|
2881
|
+
if (validationError) {
|
|
2882
|
+
try {
|
|
2883
|
+
vfsHandle.close();
|
|
2884
|
+
} catch (_) {
|
|
2885
|
+
}
|
|
2886
|
+
throw new Error(`Corrupt VFS: ${validationError}`);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
const wasFresh = vfsSize === 0;
|
|
2785
2890
|
try {
|
|
2786
2891
|
engine.init(vfsHandle, {
|
|
2787
2892
|
uid: config.uid,
|
|
2788
2893
|
gid: config.gid,
|
|
2789
2894
|
umask: config.umask,
|
|
2790
2895
|
strictPermissions: config.strictPermissions,
|
|
2791
|
-
debug: config.debug
|
|
2896
|
+
debug: config.debug,
|
|
2897
|
+
limits: activeLimits
|
|
2792
2898
|
});
|
|
2793
2899
|
} catch (err) {
|
|
2794
2900
|
try {
|