@componentor/fs 3.0.16 → 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.
@@ -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
- this.mount();
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
  }
@@ -2753,8 +2798,74 @@ async function followerLoop() {
2753
2798
  }
2754
2799
  }
2755
2800
  }
2801
+ var OPFS_SKIP = /* @__PURE__ */ new Set([".vfs.bin", ".vfs.bin.tmp"]);
2802
+ async function scanOPFSEntries(dir, prefix) {
2803
+ const result = [];
2804
+ for await (const [name, handle] of dir.entries()) {
2805
+ if (prefix === "" && OPFS_SKIP.has(name)) continue;
2806
+ const fullPath = prefix ? `${prefix}/${name}` : `/${name}`;
2807
+ if (handle.kind === "directory") {
2808
+ result.push({ path: fullPath, type: "directory" });
2809
+ result.push(...await scanOPFSEntries(handle, fullPath));
2810
+ } else {
2811
+ const file = await handle.getFile();
2812
+ const buf = await file.arrayBuffer();
2813
+ result.push({ path: fullPath, type: "file", data: new Uint8Array(buf) });
2814
+ }
2815
+ }
2816
+ return result;
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
+ }
2756
2866
  async function initEngine(config) {
2757
2867
  debug = config.debug ?? false;
2868
+ activeLimits = resolveLimits(config.limits);
2758
2869
  let rootDir = await navigator.storage.getDirectory();
2759
2870
  if (config.root && config.root !== "/") {
2760
2871
  const segments = config.root.split("/").filter(Boolean);
@@ -2764,13 +2875,26 @@ async function initEngine(config) {
2764
2875
  }
2765
2876
  const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
2766
2877
  const vfsHandle = await vfsFileHandle.createSyncAccessHandle();
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;
2767
2890
  try {
2768
2891
  engine.init(vfsHandle, {
2769
2892
  uid: config.uid,
2770
2893
  gid: config.gid,
2771
2894
  umask: config.umask,
2772
2895
  strictPermissions: config.strictPermissions,
2773
- debug: config.debug
2896
+ debug: config.debug,
2897
+ limits: activeLimits
2774
2898
  });
2775
2899
  } catch (err) {
2776
2900
  try {
@@ -2779,6 +2903,19 @@ async function initEngine(config) {
2779
2903
  }
2780
2904
  throw err;
2781
2905
  }
2906
+ if (wasFresh) {
2907
+ const opfsEntries = await scanOPFSEntries(rootDir, "");
2908
+ if (opfsEntries.length > 0) {
2909
+ const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
2910
+ for (const dir of dirs) {
2911
+ engine.mkdir(dir.path, 16877);
2912
+ }
2913
+ for (const file of opfsEntries.filter((e) => e.type === "file")) {
2914
+ engine.write(file.path, file.data);
2915
+ }
2916
+ engine.flush();
2917
+ }
2918
+ }
2782
2919
  if (config.opfsSync) {
2783
2920
  opfsSyncEnabled = true;
2784
2921
  const mc = new MessageChannel();