@componentor/fs 3.0.45 → 3.0.46

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
@@ -4204,6 +4204,16 @@ var VFSEngine = class {
4204
4204
  superblockDirty = false;
4205
4205
  // Free inode hint — skip O(n) scan
4206
4206
  freeInodeHint = 0;
4207
+ // Implicit directory support — tracks all directory prefixes implied by file paths.
4208
+ // Rebuilt lazily when pathIndex changes (tracked via generation counter).
4209
+ // Map value is the stable timestamp (ms since epoch) assigned when the implicit
4210
+ // dir was first discovered, so that stat() returns consistent mtime/ctime/atime
4211
+ // across repeated calls.
4212
+ implicitDirs = /* @__PURE__ */ new Map();
4213
+ implicitDirsGen = -1;
4214
+ // generation when implicitDirs was last rebuilt
4215
+ pathIndexGen = 0;
4216
+ // bumped on every pathIndex mutation
4207
4217
  // Configurable upper bounds
4208
4218
  maxInodes = 4e6;
4209
4219
  maxBlocks = 4e6;
@@ -4488,6 +4498,7 @@ var VFSEngine = class {
4488
4498
  }
4489
4499
  this.pathIndex.set(path, i);
4490
4500
  }
4501
+ this.pathIndexGen++;
4491
4502
  }
4492
4503
  // ========== Low-level inode I/O ==========
4493
4504
  readInode(idx) {
@@ -4808,6 +4819,7 @@ var VFSEngine = class {
4808
4819
  };
4809
4820
  this.writeInode(idx, inode);
4810
4821
  this.pathIndex.set(path, idx);
4822
+ this.pathIndexGen++;
4811
4823
  return idx;
4812
4824
  }
4813
4825
  // ========== Public API — called by server worker dispatch ==========
@@ -4964,6 +4976,7 @@ var VFSEngine = class {
4964
4976
  inode.type = INODE_TYPE.FREE;
4965
4977
  this.writeInode(idx, inode);
4966
4978
  this.pathIndex.delete(path);
4979
+ this.pathIndexGen++;
4967
4980
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
4968
4981
  this.commitPending();
4969
4982
  return { status: 0 };
@@ -4972,7 +4985,12 @@ var VFSEngine = class {
4972
4985
  stat(path) {
4973
4986
  path = this.normalizePath(path);
4974
4987
  const idx = this.resolvePathComponents(path, true);
4975
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
4988
+ if (idx === void 0) {
4989
+ if (this.isImplicitDirectory(path)) {
4990
+ return this.encodeImplicitDirStatResponse(path);
4991
+ }
4992
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
4993
+ }
4976
4994
  return this.encodeStatResponse(idx);
4977
4995
  }
4978
4996
  // ---- LSTAT (no symlink follow for the FINAL component) ----
@@ -4981,7 +4999,12 @@ var VFSEngine = class {
4981
4999
  let idx = this.resolvePathComponents(path, false);
4982
5000
  if (idx === void 0) {
4983
5001
  idx = this.resolvePathComponents(path, true);
4984
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5002
+ if (idx === void 0) {
5003
+ if (this.isImplicitDirectory(path)) {
5004
+ return this.encodeImplicitDirStatResponse(path);
5005
+ }
5006
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5007
+ }
4985
5008
  }
4986
5009
  return this.encodeStatResponse(idx);
4987
5010
  }
@@ -4990,13 +5013,17 @@ var VFSEngine = class {
4990
5013
  let nlink = inode.nlink;
4991
5014
  if (inode.type === INODE_TYPE.DIRECTORY) {
4992
5015
  const path = this.readPath(inode.pathOffset, inode.pathLength);
4993
- const children = this.getDirectChildren(path);
5016
+ const children = this.getDirectChildrenWithImplicit(path);
4994
5017
  let subdirCount = 0;
4995
5018
  for (const child of children) {
4996
- const childIdx = this.pathIndex.get(child);
4997
- if (childIdx !== void 0) {
4998
- const childInode = this.readInode(childIdx);
4999
- if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
5019
+ if (child.type === "implicit") {
5020
+ subdirCount++;
5021
+ } else {
5022
+ const childIdx = this.pathIndex.get(child.path);
5023
+ if (childIdx !== void 0) {
5024
+ const childInode = this.readInode(childIdx);
5025
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
5026
+ }
5000
5027
  }
5001
5028
  }
5002
5029
  nlink = 2 + subdirCount;
@@ -5022,7 +5049,9 @@ var VFSEngine = class {
5022
5049
  if (recursive) {
5023
5050
  return this.mkdirRecursive(path);
5024
5051
  }
5025
- if (this.pathIndex.has(path)) return { status: CODE_TO_STATUS.EEXIST, data: null };
5052
+ if (this.pathIndex.has(path) || this.isImplicitDirectory(path)) {
5053
+ return { status: CODE_TO_STATUS.EEXIST, data: null };
5054
+ }
5026
5055
  const parentStatus = this.ensureParent(path);
5027
5056
  if (parentStatus !== 0) return { status: parentStatus, data: null };
5028
5057
  const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
@@ -5057,7 +5086,26 @@ var VFSEngine = class {
5057
5086
  path = this.normalizePath(path);
5058
5087
  const recursive = (flags & 1) !== 0;
5059
5088
  const idx = this.pathIndex.get(path);
5060
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
5089
+ if (idx === void 0) {
5090
+ if (this.isImplicitDirectory(path)) {
5091
+ const children2 = this.getDirectChildrenWithImplicit(path);
5092
+ if (children2.length > 0) {
5093
+ if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
5094
+ for (const desc of this.getAllDescendants(path)) {
5095
+ const descIdx = this.pathIndex.get(desc);
5096
+ const descInode = this.readInode(descIdx);
5097
+ this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
5098
+ descInode.type = INODE_TYPE.FREE;
5099
+ this.writeInode(descIdx, descInode);
5100
+ this.pathIndex.delete(desc);
5101
+ }
5102
+ this.pathIndexGen++;
5103
+ this.commitPending();
5104
+ }
5105
+ return { status: 0 };
5106
+ }
5107
+ return { status: CODE_TO_STATUS.ENOENT };
5108
+ }
5061
5109
  const inode = this.readInode(idx);
5062
5110
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
5063
5111
  const children = this.getDirectChildren(path);
@@ -5075,6 +5123,7 @@ var VFSEngine = class {
5075
5123
  inode.type = INODE_TYPE.FREE;
5076
5124
  this.writeInode(idx, inode);
5077
5125
  this.pathIndex.delete(path);
5126
+ this.pathIndexGen++;
5078
5127
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5079
5128
  this.commitPending();
5080
5129
  return { status: 0 };
@@ -5083,20 +5132,33 @@ var VFSEngine = class {
5083
5132
  readdir(path, flags = 0) {
5084
5133
  path = this.normalizePath(path);
5085
5134
  const resolved = this.resolvePathFull(path, true);
5086
- if (!resolved) return { status: CODE_TO_STATUS.ENOENT, data: null };
5087
- const inode = this.readInode(resolved.idx);
5088
- if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5135
+ let effectiveDirPath;
5136
+ if (resolved) {
5137
+ const inode = this.readInode(resolved.idx);
5138
+ if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5139
+ effectiveDirPath = resolved.resolvedPath;
5140
+ } else if (this.isImplicitDirectory(path)) {
5141
+ effectiveDirPath = path;
5142
+ } else {
5143
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5144
+ }
5089
5145
  const withFileTypes = (flags & 1) !== 0;
5090
- const children = this.getDirectChildren(resolved.resolvedPath);
5146
+ const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
5091
5147
  if (withFileTypes) {
5092
5148
  let totalSize2 = 4;
5093
5149
  const entries = [];
5094
- for (const childPath of children) {
5095
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
5150
+ for (const child of children) {
5151
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
5096
5152
  const nameBytes = encoder10.encode(name);
5097
- const childIdx = this.pathIndex.get(childPath);
5098
- const childInode = this.readInode(childIdx);
5099
- entries.push({ name: nameBytes, type: childInode.type });
5153
+ let type;
5154
+ if (child.type === "implicit") {
5155
+ type = INODE_TYPE.DIRECTORY;
5156
+ } else {
5157
+ const childIdx = this.pathIndex.get(child.path);
5158
+ const childInode = this.readInode(childIdx);
5159
+ type = childInode.type;
5160
+ }
5161
+ entries.push({ name: nameBytes, type });
5100
5162
  totalSize2 += 2 + nameBytes.byteLength + 1;
5101
5163
  }
5102
5164
  const buf2 = new Uint8Array(totalSize2);
@@ -5114,8 +5176,8 @@ var VFSEngine = class {
5114
5176
  }
5115
5177
  let totalSize = 4;
5116
5178
  const nameEntries = [];
5117
- for (const childPath of children) {
5118
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
5179
+ for (const child of children) {
5180
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
5119
5181
  const nameBytes = encoder10.encode(name);
5120
5182
  nameEntries.push(nameBytes);
5121
5183
  totalSize += 2 + nameBytes.byteLength;
@@ -5156,6 +5218,7 @@ var VFSEngine = class {
5156
5218
  this.writeInode(idx, inode);
5157
5219
  this.pathIndex.delete(oldPath);
5158
5220
  this.pathIndex.set(newPath, idx);
5221
+ this.pathIndexGen++;
5159
5222
  if (inode.type === INODE_TYPE.DIRECTORY) {
5160
5223
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
5161
5224
  const toRename = [];
@@ -5184,7 +5247,7 @@ var VFSEngine = class {
5184
5247
  path = this.normalizePath(path);
5185
5248
  const idx = this.resolvePathComponents(path, true);
5186
5249
  const buf = new Uint8Array(1);
5187
- buf[0] = idx !== void 0 ? 1 : 0;
5250
+ buf[0] = idx !== void 0 || this.isImplicitDirectory(path) ? 1 : 0;
5188
5251
  return { status: 0, data: buf };
5189
5252
  }
5190
5253
  // ---- TRUNCATE ----
@@ -5287,7 +5350,10 @@ var VFSEngine = class {
5287
5350
  access(path, mode = 0) {
5288
5351
  path = this.normalizePath(path);
5289
5352
  const idx = this.resolvePathComponents(path, true);
5290
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
5353
+ if (idx === void 0) {
5354
+ if (this.isImplicitDirectory(path)) return { status: 0 };
5355
+ return { status: CODE_TO_STATUS.ENOENT };
5356
+ }
5291
5357
  if (mode === 0) return { status: 0 };
5292
5358
  if (!this.strictPermissions) return { status: 0 };
5293
5359
  const inode = this.readInode(idx);
@@ -5307,7 +5373,12 @@ var VFSEngine = class {
5307
5373
  realpath(path) {
5308
5374
  path = this.normalizePath(path);
5309
5375
  const idx = this.resolvePathComponents(path, true);
5310
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5376
+ if (idx === void 0) {
5377
+ if (this.isImplicitDirectory(path)) {
5378
+ return { status: 0, data: encoder10.encode(path) };
5379
+ }
5380
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5381
+ }
5311
5382
  const inode = this.readInode(idx);
5312
5383
  const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
5313
5384
  return { status: 0, data: encoder10.encode(resolvedPath) };
@@ -5496,6 +5567,7 @@ var VFSEngine = class {
5496
5567
  fstat(fd) {
5497
5568
  const entry = this.fdTable.get(fd);
5498
5569
  if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
5570
+ if (entry.implicitPath) return this.encodeImplicitDirStatResponse(entry.implicitPath);
5499
5571
  return this.encodeStatResponse(entry.inodeIdx);
5500
5572
  }
5501
5573
  // ---- FTRUNCATE ----
@@ -5518,6 +5590,7 @@ var VFSEngine = class {
5518
5590
  fchmod(fd, mode) {
5519
5591
  const entry = this.fdTable.get(fd);
5520
5592
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5593
+ if (entry.implicitPath) return { status: 0 };
5521
5594
  const inode = this.readInode(entry.inodeIdx);
5522
5595
  inode.mode = inode.mode & S_IFMT | mode & 4095;
5523
5596
  inode.ctime = Date.now();
@@ -5528,6 +5601,7 @@ var VFSEngine = class {
5528
5601
  fchown(fd, uid, gid) {
5529
5602
  const entry = this.fdTable.get(fd);
5530
5603
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5604
+ if (entry.implicitPath) return { status: 0 };
5531
5605
  const inode = this.readInode(entry.inodeIdx);
5532
5606
  inode.uid = uid;
5533
5607
  inode.gid = gid;
@@ -5539,6 +5613,7 @@ var VFSEngine = class {
5539
5613
  futimes(fd, atime, mtime) {
5540
5614
  const entry = this.fdTable.get(fd);
5541
5615
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5616
+ if (entry.implicitPath) return { status: 0 };
5542
5617
  const inode = this.readInode(entry.inodeIdx);
5543
5618
  inode.atime = atime;
5544
5619
  inode.mtime = mtime;
@@ -5550,7 +5625,16 @@ var VFSEngine = class {
5550
5625
  opendir(path, tabId) {
5551
5626
  path = this.normalizePath(path);
5552
5627
  const idx = this.resolvePathComponents(path, true);
5553
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5628
+ if (idx === void 0) {
5629
+ if (this.isImplicitDirectory(path)) {
5630
+ const fd2 = this.nextFd++;
5631
+ this.fdTable.set(fd2, { tabId, inodeIdx: -1, position: 0, flags: 0, implicitPath: path });
5632
+ const buf2 = new Uint8Array(4);
5633
+ new DataView(buf2.buffer).setUint32(0, fd2, true);
5634
+ return { status: 0, data: buf2 };
5635
+ }
5636
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5637
+ }
5554
5638
  const inode = this.readInode(idx);
5555
5639
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5556
5640
  const fd = this.nextFd++;
@@ -5589,6 +5673,106 @@ var VFSEngine = class {
5589
5673
  }
5590
5674
  return children.sort();
5591
5675
  }
5676
+ /**
5677
+ * Rebuild the set of all implicit directory paths.
5678
+ * An implicit directory is any ancestor path of a file/symlink in pathIndex
5679
+ * that doesn't itself have an explicit inode entry.
5680
+ * Only rebuilt when pathIndex has changed (tracked via generation counter).
5681
+ */
5682
+ rebuildImplicitDirs() {
5683
+ if (this.implicitDirsGen === this.pathIndexGen) return;
5684
+ const now = Date.now();
5685
+ const prev = this.implicitDirs;
5686
+ this.implicitDirs = /* @__PURE__ */ new Map();
5687
+ for (const filePath of this.pathIndex.keys()) {
5688
+ let pos = filePath.length;
5689
+ while (true) {
5690
+ pos = filePath.lastIndexOf("/", pos - 1);
5691
+ if (pos <= 0) break;
5692
+ const ancestor = filePath.substring(0, pos);
5693
+ if (this.implicitDirs.has(ancestor)) break;
5694
+ if (!this.pathIndex.has(ancestor)) {
5695
+ this.implicitDirs.set(ancestor, prev.get(ancestor) ?? now);
5696
+ }
5697
+ }
5698
+ }
5699
+ this.implicitDirsGen = this.pathIndexGen;
5700
+ }
5701
+ /**
5702
+ * Check if a path is an implicit directory (exists because files exist under it,
5703
+ * but no explicit directory inode was created for it).
5704
+ */
5705
+ isImplicitDirectory(path) {
5706
+ if (path === "/") return false;
5707
+ this.rebuildImplicitDirs();
5708
+ return this.implicitDirs.has(path);
5709
+ }
5710
+ /**
5711
+ * Get direct children of a directory path, including implicit subdirectories.
5712
+ * Returns unique child full paths. Each entry is tagged with whether it's a
5713
+ * real inode or an implicit directory.
5714
+ */
5715
+ getDirectChildrenWithImplicit(dirPath) {
5716
+ const prefix = dirPath === "/" ? "/" : dirPath + "/";
5717
+ const childNames = /* @__PURE__ */ new Map();
5718
+ for (const path of this.pathIndex.keys()) {
5719
+ if (path === dirPath) continue;
5720
+ if (!path.startsWith(prefix)) continue;
5721
+ const rest = path.substring(prefix.length);
5722
+ const slashPos = rest.indexOf("/");
5723
+ if (slashPos === -1) {
5724
+ childNames.set(rest, "real");
5725
+ } else {
5726
+ const childName = rest.substring(0, slashPos);
5727
+ if (!childNames.has(childName)) {
5728
+ const childFullPath = prefix + childName;
5729
+ childNames.set(childName, this.pathIndex.has(childFullPath) ? "real" : "implicit");
5730
+ }
5731
+ }
5732
+ }
5733
+ const result = [];
5734
+ for (const [name, type] of childNames) {
5735
+ result.push({ path: prefix + name, type });
5736
+ }
5737
+ result.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
5738
+ return result;
5739
+ }
5740
+ /**
5741
+ * Encode a synthetic stat response for an implicit directory.
5742
+ * Returns directory stats with default mode, zero size, current timestamps.
5743
+ */
5744
+ encodeImplicitDirStatResponse(path) {
5745
+ this.rebuildImplicitDirs();
5746
+ const ts = this.implicitDirs.get(path) ?? Date.now();
5747
+ const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
5748
+ const children = this.getDirectChildrenWithImplicit(path);
5749
+ let subdirCount = 0;
5750
+ for (const child of children) {
5751
+ if (child.type === "implicit") {
5752
+ subdirCount++;
5753
+ } else {
5754
+ const childIdx = this.pathIndex.get(child.path);
5755
+ if (childIdx !== void 0) {
5756
+ const childInode = this.readInode(childIdx);
5757
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
5758
+ }
5759
+ }
5760
+ }
5761
+ const nlink = 2 + subdirCount;
5762
+ const buf = new Uint8Array(53);
5763
+ const view = new DataView(buf.buffer);
5764
+ view.setUint8(0, INODE_TYPE.DIRECTORY);
5765
+ view.setUint32(1, mode, true);
5766
+ view.setFloat64(5, 0, true);
5767
+ view.setFloat64(13, ts, true);
5768
+ view.setFloat64(21, ts, true);
5769
+ view.setFloat64(29, ts, true);
5770
+ view.setUint32(37, this.processUid, true);
5771
+ view.setUint32(41, this.processGid, true);
5772
+ view.setUint32(45, 0, true);
5773
+ view.setUint32(49, nlink, true);
5774
+ return { status: 0, data: buf };
5775
+ }
5592
5776
  getAllDescendants(dirPath) {
5593
5777
  const prefix = dirPath === "/" ? "/" : dirPath + "/";
5594
5778
  const descendants = [];
@@ -5606,7 +5790,10 @@ var VFSEngine = class {
5606
5790
  if (lastSlash <= 0) return 0;
5607
5791
  const parentPath = path.substring(0, lastSlash);
5608
5792
  const parentIdx = this.pathIndex.get(parentPath);
5609
- if (parentIdx === void 0) return CODE_TO_STATUS.ENOENT;
5793
+ if (parentIdx === void 0) {
5794
+ if (this.isImplicitDirectory(parentPath)) return 0;
5795
+ return CODE_TO_STATUS.ENOENT;
5796
+ }
5610
5797
  const parentInode = this.readInode(parentIdx);
5611
5798
  if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
5612
5799
  return 0;