@componentor/fs 3.0.17 → 3.0.19

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
@@ -82,7 +82,15 @@ const fs = new VFSFileSystem({
82
82
  strictPermissions: false, // Enforce Unix permissions (default: false)
83
83
  sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
84
84
  debug: false, // Enable debug logging (default: false)
85
+ swUrl: undefined, // URL of the service worker script (default: auto-resolved)
85
86
  swScope: undefined, // Custom service worker scope (default: auto-scoped per root)
87
+ limits: { // Upper bounds for VFS validation (prevents corrupt data from causing OOM)
88
+ maxInodes: 4_000_000, // Max inode count (default: 4M)
89
+ maxBlocks: 4_000_000, // Max data blocks (default: 4M)
90
+ maxPathTable: 256 * 1024 * 1024, // Max path table bytes (default: 256MB)
91
+ maxVFSSize: 100 * 1024 * 1024 * 1024, // Max .vfs.bin size (default: 100GB)
92
+ maxPayload: 2 * 1024 * 1024 * 1024, // Max single SAB payload (default: 2GB)
93
+ },
86
94
  });
87
95
  ```
88
96
 
@@ -153,6 +161,30 @@ console.log(fs.mode); // 'hybrid'
153
161
 
154
162
  `setMode()` terminates internal workers, allocates fresh shared memory, and reinitializes the filesystem in the requested mode.
155
163
 
164
+ ### Service Worker Setup (Multi-Tab)
165
+
166
+ Multi-tab coordination requires a service worker that acts as a MessagePort broker between tabs. The built service worker is shipped at `dist/workers/service.worker.js`. Unlike regular workers (which are resolved by the bundler), **service workers must be served as a real file at a public URL**.
167
+
168
+ Most bundlers (Vite, webpack) handle `new URL('./workers/service.worker.js', import.meta.url)` automatically, but if the default resolution doesn't work in your setup, use the `swUrl` option:
169
+
170
+ ```typescript
171
+ const fs = new VFSFileSystem({
172
+ swUrl: '/vfs-service-worker.js', // your public URL
173
+ });
174
+ ```
175
+
176
+ **Vite example** — copy the file to `public/`:
177
+
178
+ ```bash
179
+ cp node_modules/@componentor/fs/dist/workers/service.worker.js public/vfs-service-worker.js
180
+ ```
181
+
182
+ ```typescript
183
+ const fs = new VFSFileSystem({ swUrl: '/vfs-service-worker.js' });
184
+ ```
185
+
186
+ If you only use a single tab, the service worker is not needed — the tab always runs as the leader.
187
+
156
188
  ## COOP/COEP Headers
157
189
 
158
190
  To enable the sync API, your page must be `crossOriginIsolated`. Add these headers:
@@ -570,6 +602,24 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
570
602
 
571
603
  ## Changelog
572
604
 
605
+ ### v3.0.19 (2026)
606
+
607
+ **Features:**
608
+ - Add `swUrl` config option to specify a custom service worker URL for multi-tab support in bundled environments where the default auto-resolved URL doesn't work
609
+ - Remove `type: 'module'` from service worker registration (built output is plain script, not ESM)
610
+
611
+ ### v3.0.18 (2026)
612
+
613
+ **Features:**
614
+ - Configurable VFS limits via `limits` option: `maxInodes`, `maxBlocks`, `maxPathTable`, `maxVFSSize`, `maxPayload`
615
+
616
+ **Fixes:**
617
+ - Pre-validate superblock before `engine.init()` to prevent hangs from corrupt values causing huge allocations
618
+ - Add upper bounds in `mount()`: max 4M inodes, 4M blocks, 256MB path table, 100GB total VFS size
619
+ - Ensure all mount errors are prefixed with `Corrupt VFS:` for consistent corruption fallback
620
+ - Cap `readPayload()` at 2GB (configurable) and validate each chunk length in the multi-chunk loop to prevent OOM/infinite loops from corrupt SAB data
621
+ - Cap `MemoryHandle.grow()` at 4GB to prevent OOM from corrupt VFS offsets on main-thread fallback
622
+
573
623
  ### v3.0.17 (2026)
574
624
 
575
625
  **Features:**
@@ -757,7 +807,7 @@ git clone https://github.com/componentor/fs
757
807
  cd fs
758
808
  npm install
759
809
  npm run build # Build the library
760
- npm test # Run unit tests (97 tests)
810
+ npm test # Run unit tests (107 tests)
761
811
  npm run benchmark:open # Run benchmarks in browser
762
812
  ```
763
813
 
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));