@componentor/fs 3.0.45 → 3.0.47

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,16 @@ var VFSEngine = class {
154
154
  superblockDirty = false;
155
155
  // Free inode hint — skip O(n) scan
156
156
  freeInodeHint = 0;
157
+ // Implicit directory support — tracks all directory prefixes implied by file paths.
158
+ // Rebuilt lazily when pathIndex changes (tracked via generation counter).
159
+ // Map value is the stable timestamp (ms since epoch) assigned when the implicit
160
+ // dir was first discovered, so that stat() returns consistent mtime/ctime/atime
161
+ // across repeated calls.
162
+ implicitDirs = /* @__PURE__ */ new Map();
163
+ implicitDirsGen = -1;
164
+ // generation when implicitDirs was last rebuilt
165
+ pathIndexGen = 0;
166
+ // bumped on every pathIndex mutation
157
167
  // Configurable upper bounds
158
168
  maxInodes = 4e6;
159
169
  maxBlocks = 4e6;
@@ -438,6 +448,7 @@ var VFSEngine = class {
438
448
  }
439
449
  this.pathIndex.set(path, i);
440
450
  }
451
+ this.pathIndexGen++;
441
452
  }
442
453
  // ========== Low-level inode I/O ==========
443
454
  readInode(idx) {
@@ -758,6 +769,7 @@ var VFSEngine = class {
758
769
  };
759
770
  this.writeInode(idx, inode);
760
771
  this.pathIndex.set(path, idx);
772
+ this.pathIndexGen++;
761
773
  return idx;
762
774
  }
763
775
  // ========== Public API — called by server worker dispatch ==========
@@ -914,6 +926,7 @@ var VFSEngine = class {
914
926
  inode.type = INODE_TYPE.FREE;
915
927
  this.writeInode(idx, inode);
916
928
  this.pathIndex.delete(path);
929
+ this.pathIndexGen++;
917
930
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
918
931
  this.commitPending();
919
932
  return { status: 0 };
@@ -922,7 +935,12 @@ var VFSEngine = class {
922
935
  stat(path) {
923
936
  path = this.normalizePath(path);
924
937
  const idx = this.resolvePathComponents(path, true);
925
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
938
+ if (idx === void 0) {
939
+ if (this.isImplicitDirectory(path)) {
940
+ return this.encodeImplicitDirStatResponse(path);
941
+ }
942
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
943
+ }
926
944
  return this.encodeStatResponse(idx);
927
945
  }
928
946
  // ---- LSTAT (no symlink follow for the FINAL component) ----
@@ -931,7 +949,12 @@ var VFSEngine = class {
931
949
  let idx = this.resolvePathComponents(path, false);
932
950
  if (idx === void 0) {
933
951
  idx = this.resolvePathComponents(path, true);
934
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
952
+ if (idx === void 0) {
953
+ if (this.isImplicitDirectory(path)) {
954
+ return this.encodeImplicitDirStatResponse(path);
955
+ }
956
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
957
+ }
935
958
  }
936
959
  return this.encodeStatResponse(idx);
937
960
  }
@@ -940,13 +963,17 @@ var VFSEngine = class {
940
963
  let nlink = inode.nlink;
941
964
  if (inode.type === INODE_TYPE.DIRECTORY) {
942
965
  const path = this.readPath(inode.pathOffset, inode.pathLength);
943
- const children = this.getDirectChildren(path);
966
+ const children = this.getDirectChildrenWithImplicit(path);
944
967
  let subdirCount = 0;
945
968
  for (const child of children) {
946
- const childIdx = this.pathIndex.get(child);
947
- if (childIdx !== void 0) {
948
- const childInode = this.readInode(childIdx);
949
- if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
969
+ if (child.type === "implicit") {
970
+ subdirCount++;
971
+ } else {
972
+ const childIdx = this.pathIndex.get(child.path);
973
+ if (childIdx !== void 0) {
974
+ const childInode = this.readInode(childIdx);
975
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
976
+ }
950
977
  }
951
978
  }
952
979
  nlink = 2 + subdirCount;
@@ -972,7 +999,9 @@ var VFSEngine = class {
972
999
  if (recursive) {
973
1000
  return this.mkdirRecursive(path);
974
1001
  }
975
- if (this.pathIndex.has(path)) return { status: CODE_TO_STATUS.EEXIST, data: null };
1002
+ if (this.pathIndex.has(path) || this.isImplicitDirectory(path)) {
1003
+ return { status: CODE_TO_STATUS.EEXIST, data: null };
1004
+ }
976
1005
  const parentStatus = this.ensureParent(path);
977
1006
  if (parentStatus !== 0) return { status: parentStatus, data: null };
978
1007
  const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
@@ -1007,7 +1036,26 @@ var VFSEngine = class {
1007
1036
  path = this.normalizePath(path);
1008
1037
  const recursive = (flags & 1) !== 0;
1009
1038
  const idx = this.pathIndex.get(path);
1010
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
1039
+ if (idx === void 0) {
1040
+ if (this.isImplicitDirectory(path)) {
1041
+ const children2 = this.getDirectChildrenWithImplicit(path);
1042
+ if (children2.length > 0) {
1043
+ if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
1044
+ for (const desc of this.getAllDescendants(path)) {
1045
+ const descIdx = this.pathIndex.get(desc);
1046
+ const descInode = this.readInode(descIdx);
1047
+ this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
1048
+ descInode.type = INODE_TYPE.FREE;
1049
+ this.writeInode(descIdx, descInode);
1050
+ this.pathIndex.delete(desc);
1051
+ }
1052
+ this.pathIndexGen++;
1053
+ this.commitPending();
1054
+ }
1055
+ return { status: 0 };
1056
+ }
1057
+ return { status: CODE_TO_STATUS.ENOENT };
1058
+ }
1011
1059
  const inode = this.readInode(idx);
1012
1060
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
1013
1061
  const children = this.getDirectChildren(path);
@@ -1025,6 +1073,7 @@ var VFSEngine = class {
1025
1073
  inode.type = INODE_TYPE.FREE;
1026
1074
  this.writeInode(idx, inode);
1027
1075
  this.pathIndex.delete(path);
1076
+ this.pathIndexGen++;
1028
1077
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
1029
1078
  this.commitPending();
1030
1079
  return { status: 0 };
@@ -1033,20 +1082,33 @@ var VFSEngine = class {
1033
1082
  readdir(path, flags = 0) {
1034
1083
  path = this.normalizePath(path);
1035
1084
  const resolved = this.resolvePathFull(path, true);
1036
- if (!resolved) return { status: CODE_TO_STATUS.ENOENT, data: null };
1037
- const inode = this.readInode(resolved.idx);
1038
- if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
1085
+ let effectiveDirPath;
1086
+ if (resolved) {
1087
+ const inode = this.readInode(resolved.idx);
1088
+ if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
1089
+ effectiveDirPath = resolved.resolvedPath;
1090
+ } else if (this.isImplicitDirectory(path)) {
1091
+ effectiveDirPath = path;
1092
+ } else {
1093
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
1094
+ }
1039
1095
  const withFileTypes = (flags & 1) !== 0;
1040
- const children = this.getDirectChildren(resolved.resolvedPath);
1096
+ const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
1041
1097
  if (withFileTypes) {
1042
1098
  let totalSize2 = 4;
1043
1099
  const entries = [];
1044
- for (const childPath of children) {
1045
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
1100
+ for (const child of children) {
1101
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
1046
1102
  const nameBytes = encoder.encode(name);
1047
- const childIdx = this.pathIndex.get(childPath);
1048
- const childInode = this.readInode(childIdx);
1049
- entries.push({ name: nameBytes, type: childInode.type });
1103
+ let type;
1104
+ if (child.type === "implicit") {
1105
+ type = INODE_TYPE.DIRECTORY;
1106
+ } else {
1107
+ const childIdx = this.pathIndex.get(child.path);
1108
+ const childInode = this.readInode(childIdx);
1109
+ type = childInode.type;
1110
+ }
1111
+ entries.push({ name: nameBytes, type });
1050
1112
  totalSize2 += 2 + nameBytes.byteLength + 1;
1051
1113
  }
1052
1114
  const buf2 = new Uint8Array(totalSize2);
@@ -1064,8 +1126,8 @@ var VFSEngine = class {
1064
1126
  }
1065
1127
  let totalSize = 4;
1066
1128
  const nameEntries = [];
1067
- for (const childPath of children) {
1068
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
1129
+ for (const child of children) {
1130
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
1069
1131
  const nameBytes = encoder.encode(name);
1070
1132
  nameEntries.push(nameBytes);
1071
1133
  totalSize += 2 + nameBytes.byteLength;
@@ -1106,6 +1168,7 @@ var VFSEngine = class {
1106
1168
  this.writeInode(idx, inode);
1107
1169
  this.pathIndex.delete(oldPath);
1108
1170
  this.pathIndex.set(newPath, idx);
1171
+ this.pathIndexGen++;
1109
1172
  if (inode.type === INODE_TYPE.DIRECTORY) {
1110
1173
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
1111
1174
  const toRename = [];
@@ -1134,7 +1197,7 @@ var VFSEngine = class {
1134
1197
  path = this.normalizePath(path);
1135
1198
  const idx = this.resolvePathComponents(path, true);
1136
1199
  const buf = new Uint8Array(1);
1137
- buf[0] = idx !== void 0 ? 1 : 0;
1200
+ buf[0] = idx !== void 0 || this.isImplicitDirectory(path) ? 1 : 0;
1138
1201
  return { status: 0, data: buf };
1139
1202
  }
1140
1203
  // ---- TRUNCATE ----
@@ -1237,7 +1300,10 @@ var VFSEngine = class {
1237
1300
  access(path, mode = 0) {
1238
1301
  path = this.normalizePath(path);
1239
1302
  const idx = this.resolvePathComponents(path, true);
1240
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
1303
+ if (idx === void 0) {
1304
+ if (this.isImplicitDirectory(path)) return { status: 0 };
1305
+ return { status: CODE_TO_STATUS.ENOENT };
1306
+ }
1241
1307
  if (mode === 0) return { status: 0 };
1242
1308
  if (!this.strictPermissions) return { status: 0 };
1243
1309
  const inode = this.readInode(idx);
@@ -1257,7 +1323,12 @@ var VFSEngine = class {
1257
1323
  realpath(path) {
1258
1324
  path = this.normalizePath(path);
1259
1325
  const idx = this.resolvePathComponents(path, true);
1260
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
1326
+ if (idx === void 0) {
1327
+ if (this.isImplicitDirectory(path)) {
1328
+ return { status: 0, data: encoder.encode(path) };
1329
+ }
1330
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
1331
+ }
1261
1332
  const inode = this.readInode(idx);
1262
1333
  const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
1263
1334
  return { status: 0, data: encoder.encode(resolvedPath) };
@@ -1446,6 +1517,7 @@ var VFSEngine = class {
1446
1517
  fstat(fd) {
1447
1518
  const entry = this.fdTable.get(fd);
1448
1519
  if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
1520
+ if (entry.implicitPath) return this.encodeImplicitDirStatResponse(entry.implicitPath);
1449
1521
  return this.encodeStatResponse(entry.inodeIdx);
1450
1522
  }
1451
1523
  // ---- FTRUNCATE ----
@@ -1468,6 +1540,7 @@ var VFSEngine = class {
1468
1540
  fchmod(fd, mode) {
1469
1541
  const entry = this.fdTable.get(fd);
1470
1542
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
1543
+ if (entry.implicitPath) return { status: 0 };
1471
1544
  const inode = this.readInode(entry.inodeIdx);
1472
1545
  inode.mode = inode.mode & S_IFMT | mode & 4095;
1473
1546
  inode.ctime = Date.now();
@@ -1478,6 +1551,7 @@ var VFSEngine = class {
1478
1551
  fchown(fd, uid, gid) {
1479
1552
  const entry = this.fdTable.get(fd);
1480
1553
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
1554
+ if (entry.implicitPath) return { status: 0 };
1481
1555
  const inode = this.readInode(entry.inodeIdx);
1482
1556
  inode.uid = uid;
1483
1557
  inode.gid = gid;
@@ -1489,6 +1563,7 @@ var VFSEngine = class {
1489
1563
  futimes(fd, atime, mtime) {
1490
1564
  const entry = this.fdTable.get(fd);
1491
1565
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
1566
+ if (entry.implicitPath) return { status: 0 };
1492
1567
  const inode = this.readInode(entry.inodeIdx);
1493
1568
  inode.atime = atime;
1494
1569
  inode.mtime = mtime;
@@ -1500,7 +1575,16 @@ var VFSEngine = class {
1500
1575
  opendir(path, tabId) {
1501
1576
  path = this.normalizePath(path);
1502
1577
  const idx = this.resolvePathComponents(path, true);
1503
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
1578
+ if (idx === void 0) {
1579
+ if (this.isImplicitDirectory(path)) {
1580
+ const fd2 = this.nextFd++;
1581
+ this.fdTable.set(fd2, { tabId, inodeIdx: -1, position: 0, flags: 0, implicitPath: path });
1582
+ const buf2 = new Uint8Array(4);
1583
+ new DataView(buf2.buffer).setUint32(0, fd2, true);
1584
+ return { status: 0, data: buf2 };
1585
+ }
1586
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
1587
+ }
1504
1588
  const inode = this.readInode(idx);
1505
1589
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
1506
1590
  const fd = this.nextFd++;
@@ -1539,6 +1623,106 @@ var VFSEngine = class {
1539
1623
  }
1540
1624
  return children.sort();
1541
1625
  }
1626
+ /**
1627
+ * Rebuild the set of all implicit directory paths.
1628
+ * An implicit directory is any ancestor path of a file/symlink in pathIndex
1629
+ * that doesn't itself have an explicit inode entry.
1630
+ * Only rebuilt when pathIndex has changed (tracked via generation counter).
1631
+ */
1632
+ rebuildImplicitDirs() {
1633
+ if (this.implicitDirsGen === this.pathIndexGen) return;
1634
+ const now = Date.now();
1635
+ const prev = this.implicitDirs;
1636
+ this.implicitDirs = /* @__PURE__ */ new Map();
1637
+ for (const filePath of this.pathIndex.keys()) {
1638
+ let pos = filePath.length;
1639
+ while (true) {
1640
+ pos = filePath.lastIndexOf("/", pos - 1);
1641
+ if (pos <= 0) break;
1642
+ const ancestor = filePath.substring(0, pos);
1643
+ if (this.implicitDirs.has(ancestor)) break;
1644
+ if (!this.pathIndex.has(ancestor)) {
1645
+ this.implicitDirs.set(ancestor, prev.get(ancestor) ?? now);
1646
+ }
1647
+ }
1648
+ }
1649
+ this.implicitDirsGen = this.pathIndexGen;
1650
+ }
1651
+ /**
1652
+ * Check if a path is an implicit directory (exists because files exist under it,
1653
+ * but no explicit directory inode was created for it).
1654
+ */
1655
+ isImplicitDirectory(path) {
1656
+ if (path === "/") return false;
1657
+ this.rebuildImplicitDirs();
1658
+ return this.implicitDirs.has(path);
1659
+ }
1660
+ /**
1661
+ * Get direct children of a directory path, including implicit subdirectories.
1662
+ * Returns unique child full paths. Each entry is tagged with whether it's a
1663
+ * real inode or an implicit directory.
1664
+ */
1665
+ getDirectChildrenWithImplicit(dirPath) {
1666
+ const prefix = dirPath === "/" ? "/" : dirPath + "/";
1667
+ const childNames = /* @__PURE__ */ new Map();
1668
+ for (const path of this.pathIndex.keys()) {
1669
+ if (path === dirPath) continue;
1670
+ if (!path.startsWith(prefix)) continue;
1671
+ const rest = path.substring(prefix.length);
1672
+ const slashPos = rest.indexOf("/");
1673
+ if (slashPos === -1) {
1674
+ childNames.set(rest, "real");
1675
+ } else {
1676
+ const childName = rest.substring(0, slashPos);
1677
+ if (!childNames.has(childName)) {
1678
+ const childFullPath = prefix + childName;
1679
+ childNames.set(childName, this.pathIndex.has(childFullPath) ? "real" : "implicit");
1680
+ }
1681
+ }
1682
+ }
1683
+ const result = [];
1684
+ for (const [name, type] of childNames) {
1685
+ result.push({ path: prefix + name, type });
1686
+ }
1687
+ result.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1688
+ return result;
1689
+ }
1690
+ /**
1691
+ * Encode a synthetic stat response for an implicit directory.
1692
+ * Returns directory stats with default mode, zero size, current timestamps.
1693
+ */
1694
+ encodeImplicitDirStatResponse(path) {
1695
+ this.rebuildImplicitDirs();
1696
+ const ts = this.implicitDirs.get(path) ?? Date.now();
1697
+ const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
1698
+ const children = this.getDirectChildrenWithImplicit(path);
1699
+ let subdirCount = 0;
1700
+ for (const child of children) {
1701
+ if (child.type === "implicit") {
1702
+ subdirCount++;
1703
+ } else {
1704
+ const childIdx = this.pathIndex.get(child.path);
1705
+ if (childIdx !== void 0) {
1706
+ const childInode = this.readInode(childIdx);
1707
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
1708
+ }
1709
+ }
1710
+ }
1711
+ const nlink = 2 + subdirCount;
1712
+ const buf = new Uint8Array(53);
1713
+ const view = new DataView(buf.buffer);
1714
+ view.setUint8(0, INODE_TYPE.DIRECTORY);
1715
+ view.setUint32(1, mode, true);
1716
+ view.setFloat64(5, 0, true);
1717
+ view.setFloat64(13, ts, true);
1718
+ view.setFloat64(21, ts, true);
1719
+ view.setFloat64(29, ts, true);
1720
+ view.setUint32(37, this.processUid, true);
1721
+ view.setUint32(41, this.processGid, true);
1722
+ view.setUint32(45, 0, true);
1723
+ view.setUint32(49, nlink, true);
1724
+ return { status: 0, data: buf };
1725
+ }
1542
1726
  getAllDescendants(dirPath) {
1543
1727
  const prefix = dirPath === "/" ? "/" : dirPath + "/";
1544
1728
  const descendants = [];
@@ -1556,7 +1740,10 @@ var VFSEngine = class {
1556
1740
  if (lastSlash <= 0) return 0;
1557
1741
  const parentPath = path.substring(0, lastSlash);
1558
1742
  const parentIdx = this.pathIndex.get(parentPath);
1559
- if (parentIdx === void 0) return CODE_TO_STATUS.ENOENT;
1743
+ if (parentIdx === void 0) {
1744
+ if (this.isImplicitDirectory(parentPath)) return 0;
1745
+ return CODE_TO_STATUS.ENOENT;
1746
+ }
1560
1747
  const parentInode = this.readInode(parentIdx);
1561
1748
  if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
1562
1749
  return 0;