@componentor/fs 3.0.8 → 3.0.10

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/dist/index.js CHANGED
@@ -1304,10 +1304,14 @@ var VFSFileSystem = class {
1304
1304
  // Ready promise for async callers
1305
1305
  readyPromise;
1306
1306
  resolveReady;
1307
+ rejectReady;
1308
+ initError = null;
1307
1309
  isReady = false;
1308
1310
  // Config (definite assignment — always set when constructor doesn't return singleton)
1309
1311
  config;
1310
1312
  tabId;
1313
+ _mode;
1314
+ corruptionError = null;
1311
1315
  /** Namespace string derived from root — used for lock names, BroadcastChannel, and SW scope
1312
1316
  * so multiple VFS instances with different roots don't collide. */
1313
1317
  ns;
@@ -1327,9 +1331,12 @@ var VFSFileSystem = class {
1327
1331
  const ns = `vfs-${root.replace(/[^a-zA-Z0-9]/g, "_")}`;
1328
1332
  const existing = instanceRegistry.get(ns);
1329
1333
  if (existing) return existing;
1334
+ const mode = config.mode ?? "hybrid";
1335
+ this._mode = mode;
1336
+ const opfsSync = config.opfsSync ?? mode === "hybrid";
1330
1337
  this.config = {
1331
1338
  root,
1332
- opfsSync: config.opfsSync ?? true,
1339
+ opfsSync,
1333
1340
  opfsSyncRoot: config.opfsSyncRoot,
1334
1341
  uid: config.uid ?? 0,
1335
1342
  gid: config.gid ?? 0,
@@ -1341,8 +1348,9 @@ var VFSFileSystem = class {
1341
1348
  };
1342
1349
  this.tabId = crypto.randomUUID();
1343
1350
  this.ns = ns;
1344
- this.readyPromise = new Promise((resolve2) => {
1351
+ this.readyPromise = new Promise((resolve2, reject) => {
1345
1352
  this.resolveReady = resolve2;
1353
+ this.rejectReady = reject;
1346
1354
  });
1347
1355
  this.promises = new VFSPromises(this._async, ns);
1348
1356
  instanceRegistry.set(ns, this);
@@ -1369,7 +1377,9 @@ var VFSFileSystem = class {
1369
1377
  this.initLeaderBroker();
1370
1378
  }
1371
1379
  } else if (msg.type === "init-failed") {
1372
- if (this.holdingLeaderLock) {
1380
+ if (msg.error?.startsWith("Corrupt VFS:")) {
1381
+ this.handleCorruptVFS(msg.error);
1382
+ } else if (this.holdingLeaderLock) {
1373
1383
  setTimeout(() => this.sendLeaderInit(), 500);
1374
1384
  } else if (!("locks" in navigator)) {
1375
1385
  this.startAsFollower();
@@ -1460,10 +1470,50 @@ var VFSFileSystem = class {
1460
1470
  }
1461
1471
  });
1462
1472
  }
1473
+ /** Send init-opfs message to sync-relay for OPFS-direct mode */
1474
+ sendOPFSInit() {
1475
+ this.syncWorker.postMessage({
1476
+ type: "init-opfs",
1477
+ sab: this.hasSAB ? this.sab : null,
1478
+ readySab: this.hasSAB ? this.readySab : null,
1479
+ asyncSab: this.hasSAB ? this.asyncSab : null,
1480
+ tabId: this.tabId,
1481
+ config: {
1482
+ root: this.config.root,
1483
+ ns: this.ns,
1484
+ uid: this.config.uid,
1485
+ gid: this.config.gid,
1486
+ debug: this.config.debug
1487
+ }
1488
+ });
1489
+ }
1490
+ /** Handle VFS corruption: log error, fall back to OPFS-direct mode.
1491
+ * The readyPromise will resolve once OPFS mode is ready, but init()
1492
+ * will reject with the corruption error to inform the caller. */
1493
+ handleCorruptVFS(errorMessage) {
1494
+ const err = new Error(`${errorMessage} \u2014 Falling back to OPFS mode`);
1495
+ this.corruptionError = err;
1496
+ console.error(`[VFS] ${err.message}`);
1497
+ if (this._mode === "vfs") {
1498
+ this.initError = err;
1499
+ this.rejectReady(err);
1500
+ if (this.hasSAB) {
1501
+ Atomics.store(this.readySignal, 0, -1);
1502
+ Atomics.notify(this.readySignal, 0);
1503
+ }
1504
+ return;
1505
+ }
1506
+ this._mode = "opfs";
1507
+ this.sendOPFSInit();
1508
+ }
1463
1509
  /** Start as leader — tell sync-relay to init VFS engine + OPFS handle */
1464
1510
  startAsLeader() {
1465
1511
  this.isFollower = false;
1466
- this.sendLeaderInit();
1512
+ if (this._mode === "opfs") {
1513
+ this.sendOPFSInit();
1514
+ } else {
1515
+ this.sendLeaderInit();
1516
+ }
1467
1517
  }
1468
1518
  /** Start as follower — connect to leader via service worker port brokering */
1469
1519
  startAsFollower() {
@@ -1564,8 +1614,9 @@ var VFSFileSystem = class {
1564
1614
  this.leaderChangeBc.close();
1565
1615
  this.leaderChangeBc = null;
1566
1616
  }
1567
- this.readyPromise = new Promise((resolve2) => {
1617
+ this.readyPromise = new Promise((resolve2, reject) => {
1568
1618
  this.resolveReady = resolve2;
1619
+ this.rejectReady = reject;
1569
1620
  });
1570
1621
  this.syncWorker.terminate();
1571
1622
  this.asyncWorker.terminate();
@@ -1586,8 +1637,12 @@ var VFSFileSystem = class {
1586
1637
  this.resolveReady();
1587
1638
  this.initLeaderBroker();
1588
1639
  } else if (msg.type === "init-failed") {
1589
- console.warn("[VFS] Promotion: OPFS handle still busy, retrying...");
1590
- setTimeout(() => this.sendLeaderInit(), 500);
1640
+ if (msg.error?.startsWith("Corrupt VFS:")) {
1641
+ this.handleCorruptVFS(msg.error);
1642
+ } else {
1643
+ console.warn("[VFS] Promotion: OPFS handle still busy, retrying...");
1644
+ setTimeout(() => this.sendLeaderInit(), 500);
1645
+ }
1591
1646
  }
1592
1647
  };
1593
1648
  this.asyncWorker.onmessage = (e) => {
@@ -1617,7 +1672,11 @@ var VFSFileSystem = class {
1617
1672
  [mc.port2]
1618
1673
  );
1619
1674
  }
1620
- this.sendLeaderInit();
1675
+ if (this._mode === "opfs") {
1676
+ this.sendOPFSInit();
1677
+ } else {
1678
+ this.sendLeaderInit();
1679
+ }
1621
1680
  }
1622
1681
  /** Spawn an inline worker from bundled code */
1623
1682
  spawnWorker(name) {
@@ -1628,14 +1687,23 @@ var VFSFileSystem = class {
1628
1687
  /** Block until workers are ready */
1629
1688
  ensureReady() {
1630
1689
  if (this.isReady) return;
1690
+ if (this.initError) throw this.initError;
1631
1691
  if (!this.hasSAB) {
1632
1692
  throw new Error("Sync API requires crossOriginIsolated (COOP/COEP headers). Use the promises API instead.");
1633
1693
  }
1634
- if (Atomics.load(this.readySignal, 0) === 1) {
1694
+ const signal = Atomics.load(this.readySignal, 0);
1695
+ if (signal === 1) {
1635
1696
  this.isReady = true;
1636
1697
  return;
1637
1698
  }
1699
+ if (signal === -1) {
1700
+ throw this.initError ?? new Error("VFS initialization failed");
1701
+ }
1638
1702
  spinWait(this.readySignal, 0, 0);
1703
+ const finalSignal = Atomics.load(this.readySignal, 0);
1704
+ if (finalSignal === -1) {
1705
+ throw this.initError ?? new Error("VFS initialization failed");
1706
+ }
1639
1707
  this.isReady = true;
1640
1708
  }
1641
1709
  /** Send a sync request via SAB and wait for response */
@@ -1886,8 +1954,101 @@ var VFSFileSystem = class {
1886
1954
  }
1887
1955
  purgeSync() {
1888
1956
  }
1889
- /** Async init helper avoid blocking main thread */
1957
+ /** The current filesystem mode. Changes to 'opfs' on corruption fallback. */
1958
+ get mode() {
1959
+ return this._mode;
1960
+ }
1961
+ /** Async init helper — avoid blocking main thread.
1962
+ * Rejects with corruption error if VFS was corrupt (but system falls back to OPFS mode).
1963
+ * Callers can catch and continue — the fs API works in OPFS mode after rejection. */
1890
1964
  init() {
1965
+ return this.readyPromise.then(() => {
1966
+ if (this.corruptionError) {
1967
+ throw this.corruptionError;
1968
+ }
1969
+ });
1970
+ }
1971
+ /** Switch the filesystem mode at runtime.
1972
+ *
1973
+ * Typical flow for IDE corruption recovery:
1974
+ * 1. `await fs.init()` throws with corruption error (auto-falls back to opfs)
1975
+ * 2. IDE shows warning, user clicks "Repair" → call `repairVFS(root, fs)`
1976
+ * 3. After repair: `await fs.setMode('hybrid')` to resume normal VFS+OPFS mode
1977
+ *
1978
+ * Returns a Promise that resolves when the new mode is ready. */
1979
+ async setMode(newMode) {
1980
+ if (newMode === this._mode && this.isReady && !this.corruptionError) {
1981
+ return;
1982
+ }
1983
+ this._mode = newMode;
1984
+ this.corruptionError = null;
1985
+ this.initError = null;
1986
+ this.isReady = false;
1987
+ this.config.opfsSync = newMode === "hybrid";
1988
+ this.readyPromise = new Promise((resolve2, reject) => {
1989
+ this.resolveReady = resolve2;
1990
+ this.rejectReady = reject;
1991
+ });
1992
+ this.syncWorker.terminate();
1993
+ this.asyncWorker.terminate();
1994
+ const sabSize = this.config.sabSize;
1995
+ if (this.hasSAB) {
1996
+ this.sab = new SharedArrayBuffer(sabSize);
1997
+ this.readySab = new SharedArrayBuffer(4);
1998
+ this.asyncSab = new SharedArrayBuffer(sabSize);
1999
+ this.ctrl = new Int32Array(this.sab, 0, 8);
2000
+ this.readySignal = new Int32Array(this.readySab, 0, 1);
2001
+ }
2002
+ this.syncWorker = this.spawnWorker("sync-relay");
2003
+ this.asyncWorker = this.spawnWorker("async-relay");
2004
+ this.syncWorker.onmessage = (e) => {
2005
+ const msg = e.data;
2006
+ if (msg.type === "ready") {
2007
+ this.isReady = true;
2008
+ this.resolveReady();
2009
+ if (!this.isFollower) {
2010
+ this.initLeaderBroker();
2011
+ }
2012
+ } else if (msg.type === "init-failed") {
2013
+ if (msg.error?.startsWith("Corrupt VFS:")) {
2014
+ this.handleCorruptVFS(msg.error);
2015
+ } else if (this.holdingLeaderLock) {
2016
+ setTimeout(() => this.sendLeaderInit(), 500);
2017
+ }
2018
+ }
2019
+ };
2020
+ this.asyncWorker.onmessage = (e) => {
2021
+ const msg = e.data;
2022
+ if (msg.type === "response") {
2023
+ const pending = this.asyncPending.get(msg.callId);
2024
+ if (pending) {
2025
+ this.asyncPending.delete(msg.callId);
2026
+ pending.resolve({ status: msg.status, data: msg.data });
2027
+ }
2028
+ }
2029
+ };
2030
+ if (this.hasSAB) {
2031
+ this.asyncWorker.postMessage({
2032
+ type: "init-leader",
2033
+ asyncSab: this.asyncSab,
2034
+ wakeSab: this.sab
2035
+ });
2036
+ } else {
2037
+ const mc = new MessageChannel();
2038
+ this.asyncWorker.postMessage(
2039
+ { type: "init-port", port: mc.port1 },
2040
+ [mc.port1]
2041
+ );
2042
+ this.syncWorker.postMessage(
2043
+ { type: "async-port", port: mc.port2 },
2044
+ [mc.port2]
2045
+ );
2046
+ }
2047
+ if (newMode === "opfs") {
2048
+ this.sendOPFSInit();
2049
+ } else {
2050
+ this.sendLeaderInit();
2051
+ }
1891
2052
  return this.readyPromise;
1892
2053
  }
1893
2054
  };
@@ -2066,6 +2227,7 @@ var VFSEngine = class {
2066
2227
  this.bitmap = new Uint8Array(layout.bitmapSize);
2067
2228
  this.handle.write(this.bitmap, { at: this.bitmapOffset });
2068
2229
  this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
2230
+ this.writeSuperblock();
2069
2231
  this.handle.flush();
2070
2232
  }
2071
2233
  /** Mount an existing VFS from disk — validates superblock integrity */
@@ -2225,14 +2387,33 @@ var VFSEngine = class {
2225
2387
  const off = i * INODE_SIZE;
2226
2388
  const type = inodeView.getUint8(off + INODE.TYPE);
2227
2389
  if (type === INODE_TYPE.FREE) continue;
2390
+ if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) {
2391
+ throw new Error(`Corrupt VFS: inode ${i} has invalid type ${type}`);
2392
+ }
2393
+ const pathOffset = inodeView.getUint32(off + INODE.PATH_OFFSET, true);
2394
+ const pathLength = inodeView.getUint16(off + INODE.PATH_LENGTH, true);
2395
+ const size = inodeView.getFloat64(off + INODE.SIZE, true);
2396
+ const firstBlock = inodeView.getUint32(off + INODE.FIRST_BLOCK, true);
2397
+ const blockCount = inodeView.getUint32(off + INODE.BLOCK_COUNT, true);
2398
+ if (pathLength === 0 || pathOffset + pathLength > this.pathTableUsed) {
2399
+ throw new Error(`Corrupt VFS: inode ${i} path out of bounds (offset=${pathOffset}, len=${pathLength}, tableUsed=${this.pathTableUsed})`);
2400
+ }
2401
+ if (type !== INODE_TYPE.DIRECTORY) {
2402
+ if (size < 0 || !isFinite(size)) {
2403
+ throw new Error(`Corrupt VFS: inode ${i} has invalid size ${size}`);
2404
+ }
2405
+ if (blockCount > 0 && firstBlock + blockCount > this.totalBlocks) {
2406
+ throw new Error(`Corrupt VFS: inode ${i} data blocks out of range (first=${firstBlock}, count=${blockCount}, total=${this.totalBlocks})`);
2407
+ }
2408
+ }
2228
2409
  const inode = {
2229
2410
  type,
2230
- pathOffset: inodeView.getUint32(off + INODE.PATH_OFFSET, true),
2231
- pathLength: inodeView.getUint16(off + INODE.PATH_LENGTH, true),
2411
+ pathOffset,
2412
+ pathLength,
2232
2413
  mode: inodeView.getUint32(off + INODE.MODE, true),
2233
- size: inodeView.getFloat64(off + INODE.SIZE, true),
2234
- firstBlock: inodeView.getUint32(off + INODE.FIRST_BLOCK, true),
2235
- blockCount: inodeView.getUint32(off + INODE.BLOCK_COUNT, true),
2414
+ size,
2415
+ firstBlock,
2416
+ blockCount,
2236
2417
  mtime: inodeView.getFloat64(off + INODE.MTIME, true),
2237
2418
  ctime: inodeView.getFloat64(off + INODE.CTIME, true),
2238
2419
  atime: inodeView.getFloat64(off + INODE.ATIME, true),
@@ -2240,7 +2421,15 @@ var VFSEngine = class {
2240
2421
  gid: inodeView.getUint32(off + INODE.GID, true)
2241
2422
  };
2242
2423
  this.inodeCache.set(i, inode);
2243
- const path = pathBuf ? decoder8.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength)) : this.readPath(inode.pathOffset, inode.pathLength);
2424
+ let path;
2425
+ if (pathBuf) {
2426
+ path = decoder8.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength));
2427
+ } else {
2428
+ path = this.readPath(inode.pathOffset, inode.pathLength);
2429
+ }
2430
+ if (!path.startsWith("/") || path.includes("\0")) {
2431
+ throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
2432
+ }
2244
2433
  this.pathIndex.set(path, i);
2245
2434
  }
2246
2435
  }
@@ -3283,7 +3472,6 @@ var MemoryHandle = class {
3283
3472
  }
3284
3473
  close() {
3285
3474
  }
3286
- /** Get the current data as an ArrayBuffer (trimmed to actual size) */
3287
3475
  getBuffer() {
3288
3476
  return this.buf.buffer.slice(0, this.len);
3289
3477
  }
@@ -3304,19 +3492,6 @@ async function openVFSHandle(fileHandle) {
3304
3492
  return { handle: new MemoryHandle(data), isMemory: true };
3305
3493
  }
3306
3494
  }
3307
- async function openFreshVFSHandle(fileHandle) {
3308
- try {
3309
- const handle = await fileHandle.createSyncAccessHandle();
3310
- return { handle, isMemory: false };
3311
- } catch {
3312
- return { handle: new MemoryHandle(), isMemory: true };
3313
- }
3314
- }
3315
- async function saveMemoryHandle(fileHandle, memHandle) {
3316
- const writable = await fileHandle.createWritable();
3317
- await writable.write(memHandle.getBuffer());
3318
- await writable.close();
3319
- }
3320
3495
  async function navigateToRoot(root) {
3321
3496
  let dir = await navigator.storage.getDirectory();
3322
3497
  if (root && root !== "/") {
@@ -3386,8 +3561,52 @@ async function readOPFSRecursive(dir, prefix, skip) {
3386
3561
  }
3387
3562
  return result;
3388
3563
  }
3389
- async function unpackToOPFS(root = "/") {
3564
+ function readVFSRecursive(fs, vfsPath) {
3565
+ const result = [];
3566
+ let entries;
3567
+ try {
3568
+ entries = fs.readdirSync(vfsPath, { withFileTypes: true });
3569
+ } catch {
3570
+ return result;
3571
+ }
3572
+ for (const entry of entries) {
3573
+ const fullPath = vfsPath === "/" ? `/${entry.name}` : `${vfsPath}/${entry.name}`;
3574
+ if (entry.isDirectory()) {
3575
+ result.push({ path: fullPath, type: "directory" });
3576
+ result.push(...readVFSRecursive(fs, fullPath));
3577
+ } else {
3578
+ try {
3579
+ const data = fs.readFileSync(fullPath);
3580
+ result.push({ path: fullPath, type: "file", data });
3581
+ } catch {
3582
+ }
3583
+ }
3584
+ }
3585
+ return result;
3586
+ }
3587
+ async function unpackToOPFS(root = "/", fs) {
3390
3588
  const rootDir = await navigateToRoot(root);
3589
+ if (fs) {
3590
+ const vfsEntries = readVFSRecursive(fs, "/");
3591
+ let files2 = 0;
3592
+ let directories2 = 0;
3593
+ for (const entry of vfsEntries) {
3594
+ if (entry.type === "directory") {
3595
+ const name = basename2(entry.path);
3596
+ const parent = await ensureParentDirs(rootDir, entry.path);
3597
+ await parent.getDirectoryHandle(name, { create: true });
3598
+ directories2++;
3599
+ } else {
3600
+ try {
3601
+ await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
3602
+ files2++;
3603
+ } catch (err) {
3604
+ console.warn(`[VFS] Failed to write OPFS file ${entry.path}: ${err.message}`);
3605
+ }
3606
+ }
3607
+ }
3608
+ return { files: files2, directories: directories2 };
3609
+ }
3391
3610
  const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
3392
3611
  const { handle } = await openVFSHandle(vfsFileHandle);
3393
3612
  let entries;
@@ -3404,183 +3623,95 @@ async function unpackToOPFS(root = "/") {
3404
3623
  for (const entry of entries) {
3405
3624
  if (entry.path === "/") continue;
3406
3625
  if (entry.type === INODE_TYPE.DIRECTORY) {
3407
- await ensureParentDirs(rootDir, entry.path + "/dummy");
3408
3626
  const name = basename2(entry.path);
3409
3627
  const parent = await ensureParentDirs(rootDir, entry.path);
3410
3628
  await parent.getDirectoryHandle(name, { create: true });
3411
3629
  directories++;
3412
- } else if (entry.type === INODE_TYPE.FILE) {
3413
- await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
3414
- files++;
3415
- } else if (entry.type === INODE_TYPE.SYMLINK) {
3630
+ } else if (entry.type === INODE_TYPE.FILE || entry.type === INODE_TYPE.SYMLINK) {
3416
3631
  await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
3417
3632
  files++;
3418
3633
  }
3419
3634
  }
3420
3635
  return { files, directories };
3421
3636
  }
3422
- async function loadFromOPFS(root = "/") {
3637
+ async function loadFromOPFS(root = "/", fs) {
3423
3638
  const rootDir = await navigateToRoot(root);
3424
3639
  const opfsEntries = await readOPFSRecursive(rootDir, "", /* @__PURE__ */ new Set([".vfs.bin"]));
3425
- try {
3426
- await rootDir.removeEntry(".vfs.bin");
3427
- } catch (_) {
3428
- }
3429
- const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
3430
- const { handle, isMemory } = await openFreshVFSHandle(vfsFileHandle);
3431
- try {
3432
- const engine = new VFSEngine();
3433
- engine.init(handle);
3640
+ if (fs) {
3641
+ try {
3642
+ const rootEntries = fs.readdirSync("/");
3643
+ for (const entry of rootEntries) {
3644
+ try {
3645
+ fs.rmSync(`/${entry}`, { recursive: true, force: true });
3646
+ } catch {
3647
+ }
3648
+ }
3649
+ } catch {
3650
+ }
3434
3651
  const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
3435
3652
  let files = 0;
3436
3653
  let directories = 0;
3437
3654
  for (const dir of dirs) {
3438
- engine.mkdir(dir.path, 16877);
3439
- directories++;
3655
+ try {
3656
+ fs.mkdirSync(dir.path, { recursive: true, mode: 493 });
3657
+ directories++;
3658
+ } catch {
3659
+ }
3440
3660
  }
3441
3661
  const fileEntries = opfsEntries.filter((e) => e.type === "file");
3442
3662
  for (const file of fileEntries) {
3443
- engine.write(file.path, new Uint8Array(file.data));
3444
- files++;
3445
- }
3446
- engine.flush();
3447
- if (isMemory) {
3448
- await saveMemoryHandle(vfsFileHandle, handle);
3663
+ try {
3664
+ const parentPath = file.path.substring(0, file.path.lastIndexOf("/")) || "/";
3665
+ if (parentPath !== "/") {
3666
+ try {
3667
+ fs.mkdirSync(parentPath, { recursive: true, mode: 493 });
3668
+ } catch {
3669
+ }
3670
+ }
3671
+ fs.writeFileSync(file.path, new Uint8Array(file.data));
3672
+ files++;
3673
+ } catch (err) {
3674
+ console.warn(`[VFS] Failed to write ${file.path}: ${err.message}`);
3675
+ }
3449
3676
  }
3450
3677
  return { files, directories };
3451
- } finally {
3452
- handle.close();
3453
- }
3454
- }
3455
- async function repairVFS(root = "/") {
3456
- const rootDir = await navigateToRoot(root);
3457
- const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
3458
- const file = await vfsFileHandle.getFile();
3459
- const raw = new Uint8Array(await file.arrayBuffer());
3460
- const fileSize = raw.byteLength;
3461
- if (fileSize < SUPERBLOCK.SIZE) {
3462
- throw new Error(`VFS file too small to repair (${fileSize} bytes)`);
3463
- }
3464
- const view = new DataView(raw.buffer);
3465
- let inodeCount;
3466
- let blockSize;
3467
- let totalBlocks;
3468
- let inodeTableOffset;
3469
- let pathTableOffset;
3470
- let dataOffset;
3471
- const magic = view.getUint32(SUPERBLOCK.MAGIC, true);
3472
- const version = view.getUint32(SUPERBLOCK.VERSION, true);
3473
- const superblockValid = magic === VFS_MAGIC && version === VFS_VERSION;
3474
- if (superblockValid) {
3475
- inodeCount = view.getUint32(SUPERBLOCK.INODE_COUNT, true);
3476
- blockSize = view.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
3477
- totalBlocks = view.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
3478
- inodeTableOffset = view.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
3479
- pathTableOffset = view.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
3480
- view.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
3481
- dataOffset = view.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
3482
- view.getUint32(SUPERBLOCK.PATH_USED, true);
3483
- if (blockSize === 0 || (blockSize & blockSize - 1) !== 0 || inodeCount === 0 || inodeTableOffset >= fileSize || pathTableOffset >= fileSize || dataOffset >= fileSize) {
3484
- const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
3485
- inodeCount = DEFAULT_INODE_COUNT;
3486
- blockSize = DEFAULT_BLOCK_SIZE;
3487
- totalBlocks = INITIAL_DATA_BLOCKS;
3488
- inodeTableOffset = layout.inodeTableOffset;
3489
- pathTableOffset = layout.pathTableOffset;
3490
- dataOffset = layout.dataOffset;
3491
- }
3492
- } else {
3493
- const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
3494
- inodeCount = DEFAULT_INODE_COUNT;
3495
- blockSize = DEFAULT_BLOCK_SIZE;
3496
- totalBlocks = INITIAL_DATA_BLOCKS;
3497
- inodeTableOffset = layout.inodeTableOffset;
3498
- pathTableOffset = layout.pathTableOffset;
3499
- dataOffset = layout.dataOffset;
3500
3678
  }
3501
- const decoder9 = new TextDecoder();
3502
- const recovered = [];
3503
- let lost = 0;
3504
- const maxInodes = Math.min(inodeCount, Math.floor((fileSize - inodeTableOffset) / INODE_SIZE));
3505
- for (let i = 0; i < maxInodes; i++) {
3506
- const off = inodeTableOffset + i * INODE_SIZE;
3507
- if (off + INODE_SIZE > fileSize) break;
3508
- const type = raw[off + INODE.TYPE];
3509
- if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) continue;
3510
- const inodeView = new DataView(raw.buffer, off, INODE_SIZE);
3511
- const pathOffset = inodeView.getUint32(INODE.PATH_OFFSET, true);
3512
- const pathLength = inodeView.getUint16(INODE.PATH_LENGTH, true);
3513
- const size = inodeView.getFloat64(INODE.SIZE, true);
3514
- const firstBlock = inodeView.getUint32(INODE.FIRST_BLOCK, true);
3515
- inodeView.getUint32(INODE.BLOCK_COUNT, true);
3516
- const absPathOffset = pathTableOffset + pathOffset;
3517
- if (pathLength === 0 || pathLength > 4096 || absPathOffset + pathLength > fileSize) {
3518
- lost++;
3519
- continue;
3520
- }
3521
- let path;
3522
- try {
3523
- path = decoder9.decode(raw.subarray(absPathOffset, absPathOffset + pathLength));
3524
- } catch {
3525
- lost++;
3526
- continue;
3527
- }
3528
- if (!path.startsWith("/") || path.includes("\0")) {
3529
- lost++;
3530
- continue;
3531
- }
3532
- if (type === INODE_TYPE.DIRECTORY) {
3533
- recovered.push({ path, type, data: new Uint8Array(0) });
3534
- continue;
3535
- }
3536
- if (size < 0 || size > fileSize || !isFinite(size)) {
3537
- lost++;
3538
- continue;
3539
- }
3540
- const dataStart = dataOffset + firstBlock * blockSize;
3541
- if (dataStart + size > fileSize || firstBlock >= totalBlocks) {
3542
- recovered.push({ path, type, data: new Uint8Array(0) });
3543
- lost++;
3544
- continue;
3545
- }
3546
- const data = raw.slice(dataStart, dataStart + size);
3547
- recovered.push({ path, type, data });
3548
- }
3549
- await rootDir.removeEntry(".vfs.bin");
3550
- const newFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
3551
- const { handle, isMemory } = await openFreshVFSHandle(newFileHandle);
3552
- try {
3553
- const engine = new VFSEngine();
3554
- engine.init(handle);
3555
- const dirs = recovered.filter((e) => e.type === INODE_TYPE.DIRECTORY && e.path !== "/").sort((a, b) => a.path.localeCompare(b.path));
3556
- const files = recovered.filter((e) => e.type === INODE_TYPE.FILE);
3557
- const symlinks = recovered.filter((e) => e.type === INODE_TYPE.SYMLINK);
3558
- for (const dir of dirs) {
3559
- const result = engine.mkdir(dir.path, 16877);
3560
- if (result.status !== 0) lost++;
3561
- }
3562
- for (const file2 of files) {
3563
- const result = engine.write(file2.path, file2.data);
3564
- if (result.status !== 0) lost++;
3565
- }
3566
- for (const sym of symlinks) {
3567
- const target = decoder9.decode(sym.data);
3568
- const result = engine.symlink(target, sym.path);
3569
- if (result.status !== 0) lost++;
3570
- }
3571
- engine.flush();
3572
- if (isMemory) {
3573
- await saveMemoryHandle(newFileHandle, handle);
3574
- }
3575
- } finally {
3576
- handle.close();
3679
+ return spawnRepairWorker({ type: "load", root });
3680
+ }
3681
+ async function repairVFS(root = "/", fs) {
3682
+ if (fs) {
3683
+ const loadResult = await loadFromOPFS(root, fs);
3684
+ await unpackToOPFS(root, fs);
3685
+ const total = loadResult.files + loadResult.directories;
3686
+ return {
3687
+ recovered: total,
3688
+ lost: 0,
3689
+ entries: []
3690
+ // Detailed entries not available in fs-based path
3691
+ };
3577
3692
  }
3578
- const entries = recovered.filter((e) => e.path !== "/").map((e) => ({
3579
- path: e.path,
3580
- type: e.type === INODE_TYPE.FILE ? "file" : e.type === INODE_TYPE.DIRECTORY ? "directory" : "symlink",
3581
- size: e.data.byteLength
3582
- }));
3583
- return { recovered: entries.length, lost, entries };
3693
+ return spawnRepairWorker({ type: "repair", root });
3694
+ }
3695
+ function spawnRepairWorker(msg) {
3696
+ return new Promise((resolve2, reject) => {
3697
+ const worker = new Worker(
3698
+ new URL("./workers/repair.worker.js", import.meta.url),
3699
+ { type: "module" }
3700
+ );
3701
+ worker.onmessage = (event) => {
3702
+ worker.terminate();
3703
+ if (event.data.error) {
3704
+ reject(new Error(event.data.error));
3705
+ } else {
3706
+ resolve2(event.data);
3707
+ }
3708
+ };
3709
+ worker.onerror = (event) => {
3710
+ worker.terminate();
3711
+ reject(new Error(event.message || "Repair worker failed"));
3712
+ };
3713
+ worker.postMessage(msg);
3714
+ });
3584
3715
  }
3585
3716
 
3586
3717
  // src/index.ts