@componentor/fs 3.0.2 → 3.0.4

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.
@@ -168,6 +168,13 @@ var VFSEngine = class {
168
168
  this.mount();
169
169
  }
170
170
  }
171
+ /** Release the sync access handle (call on fatal error or shutdown) */
172
+ closeHandle() {
173
+ try {
174
+ this.handle?.close();
175
+ } catch (_) {
176
+ }
177
+ }
171
178
  /** Format a fresh VFS */
172
179
  format() {
173
180
  const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
@@ -190,28 +197,78 @@ var VFSEngine = class {
190
197
  this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
191
198
  this.handle.flush();
192
199
  }
193
- /** Mount an existing VFS from disk */
200
+ /** Mount an existing VFS from disk — validates superblock integrity */
194
201
  mount() {
202
+ const fileSize = this.handle.getSize();
203
+ if (fileSize < SUPERBLOCK.SIZE) {
204
+ throw new Error(`Corrupt VFS: file too small (${fileSize} bytes, need at least ${SUPERBLOCK.SIZE})`);
205
+ }
195
206
  this.handle.read(this.superblockBuf, { at: 0 });
196
207
  const v = this.superblockView;
197
208
  const magic = v.getUint32(SUPERBLOCK.MAGIC, true);
198
209
  if (magic !== VFS_MAGIC) {
199
- throw new Error(`Invalid VFS: bad magic 0x${magic.toString(16)}`);
200
- }
201
- this.inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
202
- this.blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
203
- this.totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
204
- this.freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
205
- this.inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
206
- this.pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
207
- this.dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
208
- this.bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
209
- this.pathTableUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
210
- this.pathTableSize = this.bitmapOffset - this.pathTableOffset;
210
+ throw new Error(`Corrupt VFS: bad magic 0x${magic.toString(16)} (expected 0x${VFS_MAGIC.toString(16)})`);
211
+ }
212
+ const version = v.getUint32(SUPERBLOCK.VERSION, true);
213
+ if (version !== VFS_VERSION) {
214
+ throw new Error(`Corrupt VFS: unsupported version ${version} (expected ${VFS_VERSION})`);
215
+ }
216
+ const inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
217
+ const blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
218
+ const totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
219
+ const freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
220
+ const inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
221
+ const pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
222
+ const dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
223
+ const bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
224
+ const pathUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
225
+ if (blockSize === 0 || (blockSize & blockSize - 1) !== 0) {
226
+ throw new Error(`Corrupt VFS: invalid block size ${blockSize} (must be power of 2)`);
227
+ }
228
+ if (inodeCount === 0) {
229
+ throw new Error("Corrupt VFS: inode count is 0");
230
+ }
231
+ if (freeBlocks > totalBlocks) {
232
+ throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
233
+ }
234
+ if (inodeTableOffset !== SUPERBLOCK.SIZE) {
235
+ throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
236
+ }
237
+ const expectedPathOffset = inodeTableOffset + inodeCount * INODE_SIZE;
238
+ if (pathTableOffset !== expectedPathOffset) {
239
+ throw new Error(`Corrupt VFS: path table offset ${pathTableOffset} (expected ${expectedPathOffset})`);
240
+ }
241
+ if (bitmapOffset <= pathTableOffset) {
242
+ throw new Error(`Corrupt VFS: bitmap offset ${bitmapOffset} must be after path table ${pathTableOffset}`);
243
+ }
244
+ if (dataOffset <= bitmapOffset) {
245
+ throw new Error(`Corrupt VFS: data offset ${dataOffset} must be after bitmap ${bitmapOffset}`);
246
+ }
247
+ const pathTableSize = bitmapOffset - pathTableOffset;
248
+ if (pathUsed > pathTableSize) {
249
+ throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
250
+ }
251
+ const expectedMinSize = dataOffset + totalBlocks * blockSize;
252
+ if (fileSize < expectedMinSize) {
253
+ throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
254
+ }
255
+ this.inodeCount = inodeCount;
256
+ this.blockSize = blockSize;
257
+ this.totalBlocks = totalBlocks;
258
+ this.freeBlocks = freeBlocks;
259
+ this.inodeTableOffset = inodeTableOffset;
260
+ this.pathTableOffset = pathTableOffset;
261
+ this.dataOffset = dataOffset;
262
+ this.bitmapOffset = bitmapOffset;
263
+ this.pathTableUsed = pathUsed;
264
+ this.pathTableSize = pathTableSize;
211
265
  const bitmapSize = Math.ceil(this.totalBlocks / 8);
212
266
  this.bitmap = new Uint8Array(bitmapSize);
213
267
  this.handle.read(this.bitmap, { at: this.bitmapOffset });
214
268
  this.rebuildIndex();
269
+ if (!this.pathIndex.has("/")) {
270
+ throw new Error('Corrupt VFS: root directory "/" not found in inode table');
271
+ }
215
272
  }
216
273
  writeSuperblock() {
217
274
  const v = this.superblockView;
@@ -1222,6 +1279,24 @@ var VFSEngine = class {
1222
1279
  const data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
1223
1280
  return { type: inode.type, data, mtime: inode.mtime };
1224
1281
  }
1282
+ /** Export all files/dirs/symlinks from the VFS */
1283
+ exportAll() {
1284
+ const result = [];
1285
+ for (const [path, idx] of this.pathIndex) {
1286
+ const inode = this.readInode(idx);
1287
+ let data = null;
1288
+ if (inode.type === INODE_TYPE.FILE || inode.type === INODE_TYPE.SYMLINK) {
1289
+ data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
1290
+ }
1291
+ result.push({ path, type: inode.type, data, mode: inode.mode, mtime: inode.mtime });
1292
+ }
1293
+ result.sort((a, b) => {
1294
+ if (a.type === INODE_TYPE.DIRECTORY && b.type !== INODE_TYPE.DIRECTORY) return -1;
1295
+ if (a.type !== INODE_TYPE.DIRECTORY && b.type === INODE_TYPE.DIRECTORY) return 1;
1296
+ return a.path.localeCompare(b.path);
1297
+ });
1298
+ return result;
1299
+ }
1225
1300
  flush() {
1226
1301
  this.handle.flush();
1227
1302
  }
@@ -1324,6 +1399,7 @@ var leaderLoopRunning = false;
1324
1399
  var opfsSyncPort = null;
1325
1400
  var opfsSyncEnabled = false;
1326
1401
  var suppressPaths = /* @__PURE__ */ new Set();
1402
+ var watchBc = null;
1327
1403
  var sab;
1328
1404
  var ctrl;
1329
1405
  var readySab;
@@ -1450,21 +1526,21 @@ function handleRequest(reqTabId, buffer) {
1450
1526
  break;
1451
1527
  case OP.WRITE:
1452
1528
  result = engine.write(path, data ?? new Uint8Array(0), flags);
1453
- if (opfsSyncEnabled && result.status === 0) {
1529
+ if (result.status === 0) {
1454
1530
  syncOp = op;
1455
1531
  syncPath = path;
1456
1532
  }
1457
1533
  break;
1458
1534
  case OP.APPEND:
1459
1535
  result = engine.append(path, data ?? new Uint8Array(0));
1460
- if (opfsSyncEnabled && result.status === 0) {
1536
+ if (result.status === 0) {
1461
1537
  syncOp = op;
1462
1538
  syncPath = path;
1463
1539
  }
1464
1540
  break;
1465
1541
  case OP.UNLINK:
1466
1542
  result = engine.unlink(path);
1467
- if (opfsSyncEnabled && result.status === 0) {
1543
+ if (result.status === 0) {
1468
1544
  syncOp = op;
1469
1545
  syncPath = path;
1470
1546
  }
@@ -1477,14 +1553,14 @@ function handleRequest(reqTabId, buffer) {
1477
1553
  break;
1478
1554
  case OP.MKDIR:
1479
1555
  result = engine.mkdir(path, flags);
1480
- if (opfsSyncEnabled && result.status === 0) {
1556
+ if (result.status === 0) {
1481
1557
  syncOp = op;
1482
1558
  syncPath = path;
1483
1559
  }
1484
1560
  break;
1485
1561
  case OP.RMDIR:
1486
1562
  result = engine.rmdir(path, flags);
1487
- if (opfsSyncEnabled && result.status === 0) {
1563
+ if (result.status === 0) {
1488
1564
  syncOp = op;
1489
1565
  syncPath = path;
1490
1566
  }
@@ -1495,7 +1571,7 @@ function handleRequest(reqTabId, buffer) {
1495
1571
  case OP.RENAME: {
1496
1572
  const newPath = data ? decodeSecondPath(data) : "";
1497
1573
  result = engine.rename(path, newPath);
1498
- if (opfsSyncEnabled && result.status === 0) {
1574
+ if (result.status === 0) {
1499
1575
  syncOp = op;
1500
1576
  syncPath = path;
1501
1577
  syncNewPath = newPath;
@@ -1508,7 +1584,7 @@ function handleRequest(reqTabId, buffer) {
1508
1584
  case OP.TRUNCATE: {
1509
1585
  const len = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
1510
1586
  result = engine.truncate(path, len);
1511
- if (opfsSyncEnabled && result.status === 0) {
1587
+ if (result.status === 0) {
1512
1588
  syncOp = op;
1513
1589
  syncPath = path;
1514
1590
  }
@@ -1517,7 +1593,7 @@ function handleRequest(reqTabId, buffer) {
1517
1593
  case OP.COPY: {
1518
1594
  const destPath = data ? decodeSecondPath(data) : "";
1519
1595
  result = engine.copy(path, destPath, flags);
1520
- if (opfsSyncEnabled && result.status === 0) {
1596
+ if (result.status === 0) {
1521
1597
  syncOp = op;
1522
1598
  syncPath = destPath;
1523
1599
  }
@@ -1532,6 +1608,10 @@ function handleRequest(reqTabId, buffer) {
1532
1608
  case OP.CHMOD: {
1533
1609
  const chmodMode = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
1534
1610
  result = engine.chmod(path, chmodMode);
1611
+ if (result.status === 0) {
1612
+ syncOp = op;
1613
+ syncPath = path;
1614
+ }
1535
1615
  break;
1536
1616
  }
1537
1617
  case OP.CHOWN: {
@@ -1543,6 +1623,10 @@ function handleRequest(reqTabId, buffer) {
1543
1623
  const uid = dv.getUint32(0, true);
1544
1624
  const gid = dv.getUint32(4, true);
1545
1625
  result = engine.chown(path, uid, gid);
1626
+ if (result.status === 0) {
1627
+ syncOp = op;
1628
+ syncPath = path;
1629
+ }
1546
1630
  break;
1547
1631
  }
1548
1632
  case OP.UTIMES: {
@@ -1554,12 +1638,16 @@ function handleRequest(reqTabId, buffer) {
1554
1638
  const atime = dv.getFloat64(0, true);
1555
1639
  const mtime = dv.getFloat64(8, true);
1556
1640
  result = engine.utimes(path, atime, mtime);
1641
+ if (result.status === 0) {
1642
+ syncOp = op;
1643
+ syncPath = path;
1644
+ }
1557
1645
  break;
1558
1646
  }
1559
1647
  case OP.SYMLINK: {
1560
1648
  const target = data ? new TextDecoder().decode(data) : "";
1561
1649
  result = engine.symlink(target, path);
1562
- if (opfsSyncEnabled && result.status === 0) {
1650
+ if (result.status === 0) {
1563
1651
  syncOp = op;
1564
1652
  syncPath = path;
1565
1653
  }
@@ -1571,7 +1659,7 @@ function handleRequest(reqTabId, buffer) {
1571
1659
  case OP.LINK: {
1572
1660
  const newPath = data ? decodeSecondPath(data) : "";
1573
1661
  result = engine.link(path, newPath);
1574
- if (opfsSyncEnabled && result.status === 0) {
1662
+ if (result.status === 0) {
1575
1663
  syncOp = op;
1576
1664
  syncPath = newPath;
1577
1665
  }
@@ -1607,7 +1695,7 @@ function handleRequest(reqTabId, buffer) {
1607
1695
  const pos = dv.getInt32(4, true);
1608
1696
  const writeData = data.subarray(8);
1609
1697
  result = engine.fwrite(fd, writeData, pos === -1 ? null : pos);
1610
- if (opfsSyncEnabled && result.status === 0) {
1698
+ if (result.status === 0) {
1611
1699
  syncOp = op;
1612
1700
  syncPath = engine.getPathForFd(fd) ?? void 0;
1613
1701
  }
@@ -1627,7 +1715,7 @@ function handleRequest(reqTabId, buffer) {
1627
1715
  const fd = dv.getUint32(0, true);
1628
1716
  const len = dv.getUint32(4, true);
1629
1717
  result = engine.ftruncate(fd, len);
1630
- if (opfsSyncEnabled && result.status === 0) {
1718
+ if (result.status === 0) {
1631
1719
  syncOp = op;
1632
1720
  syncPath = engine.getPathForFd(fd) ?? void 0;
1633
1721
  }
@@ -1641,7 +1729,7 @@ function handleRequest(reqTabId, buffer) {
1641
1729
  break;
1642
1730
  case OP.MKDTEMP:
1643
1731
  result = engine.mkdtemp(path);
1644
- if (opfsSyncEnabled && result.status === 0 && result.data) {
1732
+ if (result.status === 0 && result.data) {
1645
1733
  syncOp = op;
1646
1734
  syncPath = new TextDecoder().decode(result.data instanceof Uint8Array ? result.data : new Uint8Array(0));
1647
1735
  }
@@ -1661,6 +1749,7 @@ function handleRequest(reqTabId, buffer) {
1661
1749
  ret._op = syncOp;
1662
1750
  ret._path = syncPath;
1663
1751
  ret._newPath = syncNewPath;
1752
+ broadcastWatch(syncOp, syncPath, syncNewPath);
1664
1753
  }
1665
1754
  return ret;
1666
1755
  }
@@ -1831,13 +1920,21 @@ async function initEngine(config) {
1831
1920
  }
1832
1921
  const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
1833
1922
  const vfsHandle = await vfsFileHandle.createSyncAccessHandle();
1834
- engine.init(vfsHandle, {
1835
- uid: config.uid,
1836
- gid: config.gid,
1837
- umask: config.umask,
1838
- strictPermissions: config.strictPermissions,
1839
- debug: config.debug
1840
- });
1923
+ try {
1924
+ engine.init(vfsHandle, {
1925
+ uid: config.uid,
1926
+ gid: config.gid,
1927
+ umask: config.umask,
1928
+ strictPermissions: config.strictPermissions,
1929
+ debug: config.debug
1930
+ });
1931
+ } catch (err) {
1932
+ try {
1933
+ vfsHandle.close();
1934
+ } catch (_) {
1935
+ }
1936
+ throw err;
1937
+ }
1841
1938
  if (config.opfsSync) {
1842
1939
  opfsSyncEnabled = true;
1843
1940
  const mc = new MessageChannel();
@@ -1851,6 +1948,39 @@ async function initEngine(config) {
1851
1948
  [mc.port2]
1852
1949
  );
1853
1950
  }
1951
+ watchBc = new BroadcastChannel("vfs-watch");
1952
+ }
1953
+ function broadcastWatch(op, path, newPath) {
1954
+ if (!watchBc) return;
1955
+ let eventType;
1956
+ switch (op) {
1957
+ case OP.WRITE:
1958
+ case OP.APPEND:
1959
+ case OP.TRUNCATE:
1960
+ case OP.FWRITE:
1961
+ case OP.FTRUNCATE:
1962
+ case OP.CHMOD:
1963
+ case OP.CHOWN:
1964
+ case OP.UTIMES:
1965
+ eventType = "change";
1966
+ break;
1967
+ case OP.UNLINK:
1968
+ case OP.RMDIR:
1969
+ case OP.RENAME:
1970
+ case OP.MKDIR:
1971
+ case OP.MKDTEMP:
1972
+ case OP.SYMLINK:
1973
+ case OP.LINK:
1974
+ case OP.COPY:
1975
+ eventType = "rename";
1976
+ break;
1977
+ default:
1978
+ return;
1979
+ }
1980
+ watchBc.postMessage({ eventType, path });
1981
+ if (op === OP.RENAME && newPath) {
1982
+ watchBc.postMessage({ eventType: "rename", path: newPath });
1983
+ }
1854
1984
  }
1855
1985
  function notifyOPFSSync(op, path, newPath) {
1856
1986
  if (!opfsSyncPort) return;
@@ -1910,6 +2040,7 @@ function handleExternalChange(msg) {
1910
2040
  case "external-write": {
1911
2041
  suppressPaths.add(msg.path);
1912
2042
  const result = engine.write(msg.path, new Uint8Array(msg.data), 0);
2043
+ if (result.status === 0) broadcastWatch(OP.WRITE, msg.path);
1913
2044
  console.log("[sync-relay] external-write:", msg.path, `${msg.data?.byteLength ?? 0}B`, `status=${result.status}`);
1914
2045
  break;
1915
2046
  }
@@ -1918,8 +2049,10 @@ function handleExternalChange(msg) {
1918
2049
  const result = engine.unlink(msg.path);
1919
2050
  if (result.status !== 0) {
1920
2051
  const rmdirResult = engine.rmdir(msg.path, 1);
2052
+ if (rmdirResult.status === 0) broadcastWatch(OP.RMDIR, msg.path);
1921
2053
  console.log("[sync-relay] external-delete (rmdir):", msg.path, `status=${rmdirResult.status}`);
1922
2054
  } else {
2055
+ broadcastWatch(OP.UNLINK, msg.path);
1923
2056
  console.log("[sync-relay] external-delete:", msg.path, `status=${result.status}`);
1924
2057
  }
1925
2058
  break;
@@ -1929,6 +2062,7 @@ function handleExternalChange(msg) {
1929
2062
  if (msg.newPath) {
1930
2063
  suppressPaths.add(msg.newPath);
1931
2064
  const result = engine.rename(msg.path, msg.newPath);
2065
+ if (result.status === 0) broadcastWatch(OP.RENAME, msg.path, msg.newPath);
1932
2066
  console.log("[sync-relay] external-rename:", msg.path, "\u2192", msg.newPath, `status=${result.status}`);
1933
2067
  }
1934
2068
  break;