@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.
package/README.md CHANGED
@@ -83,6 +83,13 @@ const fs = new VFSFileSystem({
83
83
  sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
84
84
  debug: false, // Enable debug logging (default: false)
85
85
  swScope: undefined, // Custom service worker scope (default: auto-scoped per root)
86
+ limits: { // Upper bounds for VFS validation (prevents corrupt data from causing OOM)
87
+ maxInodes: 4_000_000, // Max inode count (default: 4M)
88
+ maxBlocks: 4_000_000, // Max data blocks (default: 4M)
89
+ maxPathTable: 256 * 1024 * 1024, // Max path table bytes (default: 256MB)
90
+ maxVFSSize: 100 * 1024 * 1024 * 1024, // Max .vfs.bin size (default: 100GB)
91
+ maxPayload: 2 * 1024 * 1024 * 1024, // Max single SAB payload (default: 2GB)
92
+ },
86
93
  });
87
94
  ```
88
95
 
@@ -570,6 +577,23 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
570
577
 
571
578
  ## Changelog
572
579
 
580
+ ### v3.0.18 (2026)
581
+
582
+ **Features:**
583
+ - Configurable VFS limits via `limits` option: `maxInodes`, `maxBlocks`, `maxPathTable`, `maxVFSSize`, `maxPayload`
584
+
585
+ **Fixes:**
586
+ - Pre-validate superblock before `engine.init()` to prevent hangs from corrupt values causing huge allocations
587
+ - Add upper bounds in `mount()`: max 4M inodes, 4M blocks, 256MB path table, 100GB total VFS size
588
+ - Ensure all mount errors are prefixed with `Corrupt VFS:` for consistent corruption fallback
589
+ - Cap `readPayload()` at 2GB (configurable) and validate each chunk length in the multi-chunk loop to prevent OOM/infinite loops from corrupt SAB data
590
+ - Cap `MemoryHandle.grow()` at 4GB to prevent OOM from corrupt VFS offsets on main-thread fallback
591
+
592
+ ### v3.0.17 (2026)
593
+
594
+ **Features:**
595
+ - Auto-populate VFS from existing OPFS files when `.vfs.bin` doesn't exist — seamless transition from OPFS mode back to hybrid mode without manual `loadFromOPFS()` call
596
+
573
597
  ### v3.0.16 (2026)
574
598
 
575
599
  **Fixes:**
@@ -752,7 +776,7 @@ git clone https://github.com/componentor/fs
752
776
  cd fs
753
777
  npm install
754
778
  npm run build # Build the library
755
- npm test # Run unit tests (97 tests)
779
+ npm test # Run unit tests (107 tests)
756
780
  npm run benchmark:open # Run benchmarks in browser
757
781
  ```
758
782
 
package/dist/index.d.mts CHANGED
@@ -140,6 +140,19 @@ type WatchFileListener = (curr: Stats, prev: Stats) => void;
140
140
  * Automatically selected as fallback when VFS corruption is detected in hybrid mode.
141
141
  */
142
142
  type FSMode = 'hybrid' | 'vfs' | 'opfs';
143
+ /** Upper bounds for VFS validation. Prevents corrupt data from causing OOM/hangs. */
144
+ interface VFSLimits {
145
+ /** Maximum number of inodes (default: 4,000,000) */
146
+ maxInodes?: number;
147
+ /** Maximum number of data blocks (default: 4,000,000) */
148
+ maxBlocks?: number;
149
+ /** Maximum path table size in bytes (default: 256MB) */
150
+ maxPathTable?: number;
151
+ /** Maximum total VFS file size in bytes (default: 100GB) */
152
+ maxVFSSize?: number;
153
+ /** Maximum single SAB payload size in bytes (default: 2GB) */
154
+ maxPayload?: number;
155
+ }
143
156
  /** VFS configuration options */
144
157
  interface VFSConfig {
145
158
  root?: string;
@@ -158,6 +171,8 @@ interface VFSConfig {
158
171
  * `'./opfs-fs-sw/'` (relative to the SW script URL) so it won't collide
159
172
  * with the host application's service worker. */
160
173
  swScope?: string;
174
+ /** Upper bounds for VFS validation (prevents corrupt data from causing OOM/hangs). */
175
+ limits?: VFSLimits;
161
176
  }
162
177
 
163
178
  type AsyncRequestFn = (op: number, path: string, flags?: number, data?: Uint8Array | string | null, path2?: string, fdArgs?: Record<string, unknown>) => Promise<{
@@ -506,4 +521,4 @@ declare function getDefaultFS(): VFSFileSystem;
506
521
  /** Async init helper — avoids blocking main thread */
507
522
  declare function init(): Promise<void>;
508
523
 
509
- export { type Dir, type Dirent, type Encoding, FSError, type FSMode, type FSWatcher, type FileHandle, type LoadResult, type MkdirOptions, type PathLike, type ReadOptions, type ReadStreamOptions, type ReaddirOptions, type RepairResult, type RmOptions, type RmdirOptions, type Stats, type UnpackResult, type VFSConfig, VFSFileSystem, type WatchEventType, type WatchFileListener, type WatchListener, type WatchOptions, type WriteOptions, type WriteStreamOptions, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path, repairVFS, statusToError, unpackToOPFS };
524
+ export { type Dir, type Dirent, type Encoding, FSError, type FSMode, type FSWatcher, type FileHandle, type LoadResult, type MkdirOptions, type PathLike, type ReadOptions, type ReadStreamOptions, type ReaddirOptions, type RepairResult, type RmOptions, type RmdirOptions, type Stats, type UnpackResult, type VFSConfig, VFSFileSystem, type VFSLimits, type WatchEventType, type WatchFileListener, type WatchListener, type WatchOptions, type WriteOptions, type WriteStreamOptions, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path, repairVFS, statusToError, unpackToOPFS };
package/dist/index.js CHANGED
@@ -1351,7 +1351,8 @@ var VFSFileSystem = class {
1351
1351
  strictPermissions: config.strictPermissions ?? false,
1352
1352
  sabSize: config.sabSize ?? DEFAULT_SAB_SIZE,
1353
1353
  debug: config.debug ?? false,
1354
- swScope: config.swScope
1354
+ swScope: config.swScope,
1355
+ limits: config.limits
1355
1356
  };
1356
1357
  this.tabId = crypto.randomUUID();
1357
1358
  this.ns = ns;
@@ -1473,7 +1474,8 @@ var VFSFileSystem = class {
1473
1474
  gid: this.config.gid,
1474
1475
  umask: this.config.umask,
1475
1476
  strictPermissions: this.config.strictPermissions,
1476
- debug: this.config.debug
1477
+ debug: this.config.debug,
1478
+ limits: this.config.limits
1477
1479
  }
1478
1480
  });
1479
1481
  }
@@ -2193,6 +2195,13 @@ var VFSEngine = class {
2193
2195
  superblockDirty = false;
2194
2196
  // Free inode hint — skip O(n) scan
2195
2197
  freeInodeHint = 0;
2198
+ // Configurable upper bounds
2199
+ maxInodes = 4e6;
2200
+ maxBlocks = 4e6;
2201
+ maxPathTable = 256 * 1024 * 1024;
2202
+ // 256MB
2203
+ maxVFSSize = 100 * 1024 * 1024 * 1024;
2204
+ // 100GB
2196
2205
  init(handle, opts) {
2197
2206
  this.handle = handle;
2198
2207
  this.processUid = opts?.uid ?? 0;
@@ -2200,11 +2209,23 @@ var VFSEngine = class {
2200
2209
  this.umask = opts?.umask ?? DEFAULT_UMASK;
2201
2210
  this.strictPermissions = opts?.strictPermissions ?? false;
2202
2211
  this.debug = opts?.debug ?? false;
2212
+ if (opts?.limits) {
2213
+ if (opts.limits.maxInodes != null) this.maxInodes = opts.limits.maxInodes;
2214
+ if (opts.limits.maxBlocks != null) this.maxBlocks = opts.limits.maxBlocks;
2215
+ if (opts.limits.maxPathTable != null) this.maxPathTable = opts.limits.maxPathTable;
2216
+ if (opts.limits.maxVFSSize != null) this.maxVFSSize = opts.limits.maxVFSSize;
2217
+ }
2203
2218
  const size = handle.getSize();
2204
2219
  if (size === 0) {
2205
2220
  this.format();
2206
2221
  } else {
2207
- this.mount();
2222
+ try {
2223
+ this.mount();
2224
+ } catch (err) {
2225
+ const msg = err.message ?? String(err);
2226
+ if (msg.startsWith("Corrupt VFS:")) throw err;
2227
+ throw new Error(`Corrupt VFS: ${msg}`);
2228
+ }
2208
2229
  }
2209
2230
  }
2210
2231
  /** Release the sync access handle (call on fatal error or shutdown) */
@@ -2271,6 +2292,18 @@ var VFSEngine = class {
2271
2292
  if (freeBlocks > totalBlocks) {
2272
2293
  throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
2273
2294
  }
2295
+ if (inodeCount > this.maxInodes) {
2296
+ throw new Error(`Corrupt VFS: inode count ${inodeCount} exceeds maximum ${this.maxInodes}`);
2297
+ }
2298
+ if (totalBlocks > this.maxBlocks) {
2299
+ throw new Error(`Corrupt VFS: total blocks ${totalBlocks} exceeds maximum ${this.maxBlocks}`);
2300
+ }
2301
+ if (fileSize > this.maxVFSSize) {
2302
+ throw new Error(`Corrupt VFS: file size ${fileSize} exceeds maximum ${this.maxVFSSize}`);
2303
+ }
2304
+ if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) {
2305
+ throw new Error(`Corrupt VFS: non-finite or negative section offset`);
2306
+ }
2274
2307
  if (inodeTableOffset !== SUPERBLOCK.SIZE) {
2275
2308
  throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
2276
2309
  }
@@ -2288,7 +2321,13 @@ var VFSEngine = class {
2288
2321
  if (pathUsed > pathTableSize) {
2289
2322
  throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
2290
2323
  }
2324
+ if (pathTableSize > this.maxPathTable) {
2325
+ throw new Error(`Corrupt VFS: path table size ${pathTableSize} exceeds maximum ${this.maxPathTable}`);
2326
+ }
2291
2327
  const expectedMinSize = dataOffset + totalBlocks * blockSize;
2328
+ if (expectedMinSize > this.maxVFSSize) {
2329
+ throw new Error(`Corrupt VFS: computed layout size ${expectedMinSize} exceeds maximum ${this.maxVFSSize}`);
2330
+ }
2292
2331
  if (fileSize < expectedMinSize) {
2293
2332
  throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
2294
2333
  }
@@ -3483,6 +3522,10 @@ var MemoryHandle = class {
3483
3522
  return this.buf.buffer.slice(0, this.len);
3484
3523
  }
3485
3524
  grow(minSize) {
3525
+ const MAX_SIZE = 4 * 1024 * 1024 * 1024;
3526
+ if (minSize > MAX_SIZE) {
3527
+ throw new Error(`MemoryHandle: cannot grow to ${minSize} bytes (max ${MAX_SIZE})`);
3528
+ }
3486
3529
  const newSize = Math.max(minSize, this.buf.length * 2);
3487
3530
  const newBuf = new Uint8Array(newSize);
3488
3531
  newBuf.set(this.buf.subarray(0, this.len));