@componentor/fs 3.0.44 → 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) {
@@ -4560,14 +4571,23 @@ var VFSEngine = class {
4560
4571
  growPathTable(needed) {
4561
4572
  const newSize = Math.max(this.pathTableSize * 2, needed + INITIAL_PATH_TABLE_SIZE);
4562
4573
  const growth = newSize - this.pathTableSize;
4563
- const dataSize = this.totalBlocks * this.blockSize;
4564
- const dataBuf = new Uint8Array(dataSize);
4565
- this.handle.read(dataBuf, { at: this.dataOffset });
4566
4574
  const newTotalSize = this.handle.getSize() + growth;
4567
4575
  this.handle.truncate(newTotalSize);
4576
+ const dataSize = this.totalBlocks * this.blockSize;
4577
+ const CHUNK = 4 * 1024 * 1024;
4578
+ const scratch = new Uint8Array(Math.min(CHUNK, Math.max(dataSize, 1)));
4579
+ let remaining = dataSize;
4580
+ while (remaining > 0) {
4581
+ const chunk = Math.min(remaining, CHUNK);
4582
+ const srcAt = this.dataOffset + (remaining - chunk);
4583
+ const dstAt = this.dataOffset + growth + (remaining - chunk);
4584
+ const slice = chunk < scratch.length ? scratch.subarray(0, chunk) : scratch;
4585
+ this.handle.read(slice, { at: srcAt });
4586
+ this.handle.write(slice, { at: dstAt });
4587
+ remaining -= chunk;
4588
+ }
4568
4589
  const newBitmapOffset = this.bitmapOffset + growth;
4569
4590
  const newDataOffset = this.dataOffset + growth;
4570
- this.handle.write(dataBuf, { at: newDataOffset });
4571
4591
  this.handle.write(this.bitmap, { at: newBitmapOffset });
4572
4592
  this.pathTableSize = newSize;
4573
4593
  this.bitmapOffset = newBitmapOffset;
@@ -4575,6 +4595,23 @@ var VFSEngine = class {
4575
4595
  this.superblockDirty = true;
4576
4596
  }
4577
4597
  // ========== Bitmap I/O ==========
4598
+ // Write `length` zero bytes at absolute file offset `at` via a small
4599
+ // reusable scratch buffer. Used to materialize POSIX "holes" when a
4600
+ // write starts past the current file size — those bytes must read as
4601
+ // zeros rather than whatever stale data happened to live in the
4602
+ // underlying storage blocks.
4603
+ zeroFileRange(at, length) {
4604
+ if (length <= 0) return;
4605
+ const CHUNK = 4 * 1024 * 1024;
4606
+ const zeros = new Uint8Array(Math.min(length, CHUNK));
4607
+ let written = 0;
4608
+ while (written < length) {
4609
+ const n = Math.min(CHUNK, length - written);
4610
+ const slice = n < zeros.length ? zeros.subarray(0, n) : zeros;
4611
+ this.handle.write(slice, { at: at + written });
4612
+ written += n;
4613
+ }
4614
+ }
4578
4615
  allocateBlocks(count) {
4579
4616
  if (count === 0) return 0;
4580
4617
  const bitmap = this.bitmap;
@@ -4782,6 +4819,7 @@ var VFSEngine = class {
4782
4819
  };
4783
4820
  this.writeInode(idx, inode);
4784
4821
  this.pathIndex.set(path, idx);
4822
+ this.pathIndexGen++;
4785
4823
  return idx;
4786
4824
  }
4787
4825
  // ========== Public API — called by server worker dispatch ==========
@@ -4899,17 +4937,28 @@ var VFSEngine = class {
4899
4937
  }
4900
4938
  const inode = this.readInode(existingIdx);
4901
4939
  if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
4902
- const existing = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
4903
- const combined = new Uint8Array(existing.byteLength + data.byteLength);
4904
- combined.set(existing);
4905
- combined.set(data, existing.byteLength);
4906
- const neededBlocks = Math.ceil(combined.byteLength / this.blockSize);
4907
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
4940
+ const combinedSize = inode.size + data.byteLength;
4941
+ const neededBlocks = Math.ceil(combinedSize / this.blockSize);
4908
4942
  const newFirst = this.allocateBlocks(neededBlocks);
4909
- this.writeData(newFirst, combined);
4943
+ const newBase = this.dataOffset + newFirst * this.blockSize;
4944
+ if (inode.size > 0) {
4945
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
4946
+ const CHUNK = 4 * 1024 * 1024;
4947
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
4948
+ let copied = 0;
4949
+ while (copied < inode.size) {
4950
+ const n = Math.min(CHUNK, inode.size - copied);
4951
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
4952
+ this.handle.read(slice, { at: oldBase + copied });
4953
+ this.handle.write(slice, { at: newBase + copied });
4954
+ copied += n;
4955
+ }
4956
+ }
4957
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
4958
+ this.handle.write(data, { at: newBase + inode.size });
4910
4959
  inode.firstBlock = newFirst;
4911
4960
  inode.blockCount = neededBlocks;
4912
- inode.size = combined.byteLength;
4961
+ inode.size = combinedSize;
4913
4962
  inode.mtime = Date.now();
4914
4963
  this.writeInode(existingIdx, inode);
4915
4964
  this.commitPending();
@@ -4927,6 +4976,7 @@ var VFSEngine = class {
4927
4976
  inode.type = INODE_TYPE.FREE;
4928
4977
  this.writeInode(idx, inode);
4929
4978
  this.pathIndex.delete(path);
4979
+ this.pathIndexGen++;
4930
4980
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
4931
4981
  this.commitPending();
4932
4982
  return { status: 0 };
@@ -4935,7 +4985,12 @@ var VFSEngine = class {
4935
4985
  stat(path) {
4936
4986
  path = this.normalizePath(path);
4937
4987
  const idx = this.resolvePathComponents(path, true);
4938
- 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
+ }
4939
4994
  return this.encodeStatResponse(idx);
4940
4995
  }
4941
4996
  // ---- LSTAT (no symlink follow for the FINAL component) ----
@@ -4944,7 +4999,12 @@ var VFSEngine = class {
4944
4999
  let idx = this.resolvePathComponents(path, false);
4945
5000
  if (idx === void 0) {
4946
5001
  idx = this.resolvePathComponents(path, true);
4947
- 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
+ }
4948
5008
  }
4949
5009
  return this.encodeStatResponse(idx);
4950
5010
  }
@@ -4953,13 +5013,17 @@ var VFSEngine = class {
4953
5013
  let nlink = inode.nlink;
4954
5014
  if (inode.type === INODE_TYPE.DIRECTORY) {
4955
5015
  const path = this.readPath(inode.pathOffset, inode.pathLength);
4956
- const children = this.getDirectChildren(path);
5016
+ const children = this.getDirectChildrenWithImplicit(path);
4957
5017
  let subdirCount = 0;
4958
5018
  for (const child of children) {
4959
- const childIdx = this.pathIndex.get(child);
4960
- if (childIdx !== void 0) {
4961
- const childInode = this.readInode(childIdx);
4962
- 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
+ }
4963
5027
  }
4964
5028
  }
4965
5029
  nlink = 2 + subdirCount;
@@ -4985,7 +5049,9 @@ var VFSEngine = class {
4985
5049
  if (recursive) {
4986
5050
  return this.mkdirRecursive(path);
4987
5051
  }
4988
- 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
+ }
4989
5055
  const parentStatus = this.ensureParent(path);
4990
5056
  if (parentStatus !== 0) return { status: parentStatus, data: null };
4991
5057
  const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
@@ -5020,7 +5086,26 @@ var VFSEngine = class {
5020
5086
  path = this.normalizePath(path);
5021
5087
  const recursive = (flags & 1) !== 0;
5022
5088
  const idx = this.pathIndex.get(path);
5023
- 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
+ }
5024
5109
  const inode = this.readInode(idx);
5025
5110
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
5026
5111
  const children = this.getDirectChildren(path);
@@ -5038,6 +5123,7 @@ var VFSEngine = class {
5038
5123
  inode.type = INODE_TYPE.FREE;
5039
5124
  this.writeInode(idx, inode);
5040
5125
  this.pathIndex.delete(path);
5126
+ this.pathIndexGen++;
5041
5127
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5042
5128
  this.commitPending();
5043
5129
  return { status: 0 };
@@ -5046,20 +5132,33 @@ var VFSEngine = class {
5046
5132
  readdir(path, flags = 0) {
5047
5133
  path = this.normalizePath(path);
5048
5134
  const resolved = this.resolvePathFull(path, true);
5049
- if (!resolved) return { status: CODE_TO_STATUS.ENOENT, data: null };
5050
- const inode = this.readInode(resolved.idx);
5051
- 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
+ }
5052
5145
  const withFileTypes = (flags & 1) !== 0;
5053
- const children = this.getDirectChildren(resolved.resolvedPath);
5146
+ const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
5054
5147
  if (withFileTypes) {
5055
5148
  let totalSize2 = 4;
5056
5149
  const entries = [];
5057
- for (const childPath of children) {
5058
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
5150
+ for (const child of children) {
5151
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
5059
5152
  const nameBytes = encoder10.encode(name);
5060
- const childIdx = this.pathIndex.get(childPath);
5061
- const childInode = this.readInode(childIdx);
5062
- 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 });
5063
5162
  totalSize2 += 2 + nameBytes.byteLength + 1;
5064
5163
  }
5065
5164
  const buf2 = new Uint8Array(totalSize2);
@@ -5077,8 +5176,8 @@ var VFSEngine = class {
5077
5176
  }
5078
5177
  let totalSize = 4;
5079
5178
  const nameEntries = [];
5080
- for (const childPath of children) {
5081
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
5179
+ for (const child of children) {
5180
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
5082
5181
  const nameBytes = encoder10.encode(name);
5083
5182
  nameEntries.push(nameBytes);
5084
5183
  totalSize += 2 + nameBytes.byteLength;
@@ -5119,6 +5218,7 @@ var VFSEngine = class {
5119
5218
  this.writeInode(idx, inode);
5120
5219
  this.pathIndex.delete(oldPath);
5121
5220
  this.pathIndex.set(newPath, idx);
5221
+ this.pathIndexGen++;
5122
5222
  if (inode.type === INODE_TYPE.DIRECTORY) {
5123
5223
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
5124
5224
  const toRename = [];
@@ -5147,7 +5247,7 @@ var VFSEngine = class {
5147
5247
  path = this.normalizePath(path);
5148
5248
  const idx = this.resolvePathComponents(path, true);
5149
5249
  const buf = new Uint8Array(1);
5150
- buf[0] = idx !== void 0 ? 1 : 0;
5250
+ buf[0] = idx !== void 0 || this.isImplicitDirectory(path) ? 1 : 0;
5151
5251
  return { status: 0, data: buf };
5152
5252
  }
5153
5253
  // ---- TRUNCATE ----
@@ -5172,13 +5272,29 @@ var VFSEngine = class {
5172
5272
  } else if (len > inode.size) {
5173
5273
  const neededBlocks = Math.ceil(len / this.blockSize);
5174
5274
  if (neededBlocks > inode.blockCount) {
5175
- const oldData = this.readData(inode.firstBlock, inode.blockCount, inode.size);
5176
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
5177
5275
  const newFirst = this.allocateBlocks(neededBlocks);
5178
- const newData = new Uint8Array(len);
5179
- newData.set(oldData);
5180
- this.writeData(newFirst, newData);
5276
+ const newBase = this.dataOffset + newFirst * this.blockSize;
5277
+ if (inode.size > 0) {
5278
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
5279
+ const CHUNK = 4 * 1024 * 1024;
5280
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
5281
+ let copied = 0;
5282
+ while (copied < inode.size) {
5283
+ const n = Math.min(CHUNK, inode.size - copied);
5284
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
5285
+ this.handle.read(slice, { at: oldBase + copied });
5286
+ this.handle.write(slice, { at: newBase + copied });
5287
+ copied += n;
5288
+ }
5289
+ }
5290
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
5291
+ this.zeroFileRange(newBase + inode.size, len - inode.size);
5181
5292
  inode.firstBlock = newFirst;
5293
+ } else {
5294
+ this.zeroFileRange(
5295
+ this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
5296
+ len - inode.size
5297
+ );
5182
5298
  }
5183
5299
  inode.blockCount = neededBlocks;
5184
5300
  inode.size = len;
@@ -5199,14 +5315,45 @@ var VFSEngine = class {
5199
5315
  if (flags & 1 && this.pathIndex.has(destPath)) {
5200
5316
  return { status: CODE_TO_STATUS.EEXIST };
5201
5317
  }
5202
- const data = srcInode.size > 0 ? this.readData(srcInode.firstBlock, srcInode.blockCount, srcInode.size) : new Uint8Array(0);
5203
- return this.write(destPath, data);
5318
+ if (srcPath === destPath) return { status: 0 };
5319
+ const srcSize = srcInode.size;
5320
+ const srcFirstBlock = srcInode.firstBlock;
5321
+ const emptyStatus = this.write(destPath, new Uint8Array(0));
5322
+ if (emptyStatus.status !== 0) return emptyStatus;
5323
+ if (srcSize === 0) return { status: 0 };
5324
+ const destIdx = this.resolvePathComponents(destPath, true);
5325
+ if (destIdx === void 0) return { status: CODE_TO_STATUS.EIO };
5326
+ const destInode = this.readInode(destIdx);
5327
+ const neededBlocks = Math.ceil(srcSize / this.blockSize);
5328
+ const newFirst = this.allocateBlocks(neededBlocks);
5329
+ const newBase = this.dataOffset + newFirst * this.blockSize;
5330
+ const srcBase = this.dataOffset + srcFirstBlock * this.blockSize;
5331
+ const CHUNK = 4 * 1024 * 1024;
5332
+ const scratch = new Uint8Array(Math.min(CHUNK, srcSize));
5333
+ let copied = 0;
5334
+ while (copied < srcSize) {
5335
+ const n = Math.min(CHUNK, srcSize - copied);
5336
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
5337
+ this.handle.read(slice, { at: srcBase + copied });
5338
+ this.handle.write(slice, { at: newBase + copied });
5339
+ copied += n;
5340
+ }
5341
+ destInode.firstBlock = newFirst;
5342
+ destInode.blockCount = neededBlocks;
5343
+ destInode.size = srcSize;
5344
+ destInode.mtime = Date.now();
5345
+ this.writeInode(destIdx, destInode);
5346
+ this.commitPending();
5347
+ return { status: 0 };
5204
5348
  }
5205
5349
  // ---- ACCESS ----
5206
5350
  access(path, mode = 0) {
5207
5351
  path = this.normalizePath(path);
5208
5352
  const idx = this.resolvePathComponents(path, true);
5209
- 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
+ }
5210
5357
  if (mode === 0) return { status: 0 };
5211
5358
  if (!this.strictPermissions) return { status: 0 };
5212
5359
  const inode = this.readInode(idx);
@@ -5226,7 +5373,12 @@ var VFSEngine = class {
5226
5373
  realpath(path) {
5227
5374
  path = this.normalizePath(path);
5228
5375
  const idx = this.resolvePathComponents(path, true);
5229
- 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
+ }
5230
5382
  const inode = this.readInode(idx);
5231
5383
  const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
5232
5384
  return { status: 0, data: encoder10.encode(resolvedPath) };
@@ -5364,16 +5516,35 @@ var VFSEngine = class {
5364
5516
  if (endPos > inode.size) {
5365
5517
  const neededBlocks = Math.ceil(endPos / this.blockSize);
5366
5518
  if (neededBlocks > inode.blockCount) {
5367
- const oldData = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
5368
- this.freeBlockRange(inode.firstBlock, inode.blockCount);
5369
5519
  const newFirst = this.allocateBlocks(neededBlocks);
5370
- const newBuf = new Uint8Array(endPos);
5371
- newBuf.set(oldData);
5372
- newBuf.set(data, pos);
5373
- this.writeData(newFirst, newBuf);
5520
+ const newBase = this.dataOffset + newFirst * this.blockSize;
5521
+ const oldBase = this.dataOffset + inode.firstBlock * this.blockSize;
5522
+ if (inode.size > 0) {
5523
+ const CHUNK = 4 * 1024 * 1024;
5524
+ const scratch = new Uint8Array(Math.min(CHUNK, inode.size));
5525
+ let copied = 0;
5526
+ while (copied < inode.size) {
5527
+ const n = Math.min(CHUNK, inode.size - copied);
5528
+ const slice = n < scratch.length ? scratch.subarray(0, n) : scratch;
5529
+ this.handle.read(slice, { at: oldBase + copied });
5530
+ this.handle.write(slice, { at: newBase + copied });
5531
+ copied += n;
5532
+ }
5533
+ }
5534
+ this.freeBlockRange(inode.firstBlock, inode.blockCount);
5535
+ if (pos > inode.size) {
5536
+ this.zeroFileRange(newBase + inode.size, pos - inode.size);
5537
+ }
5538
+ this.handle.write(data, { at: newBase + pos });
5374
5539
  inode.firstBlock = newFirst;
5375
5540
  inode.blockCount = neededBlocks;
5376
5541
  } else {
5542
+ if (pos > inode.size) {
5543
+ this.zeroFileRange(
5544
+ this.dataOffset + inode.firstBlock * this.blockSize + inode.size,
5545
+ pos - inode.size
5546
+ );
5547
+ }
5377
5548
  const dataOffset = this.dataOffset + inode.firstBlock * this.blockSize + pos;
5378
5549
  this.handle.write(data, { at: dataOffset });
5379
5550
  }
@@ -5396,6 +5567,7 @@ var VFSEngine = class {
5396
5567
  fstat(fd) {
5397
5568
  const entry = this.fdTable.get(fd);
5398
5569
  if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
5570
+ if (entry.implicitPath) return this.encodeImplicitDirStatResponse(entry.implicitPath);
5399
5571
  return this.encodeStatResponse(entry.inodeIdx);
5400
5572
  }
5401
5573
  // ---- FTRUNCATE ----
@@ -5418,6 +5590,7 @@ var VFSEngine = class {
5418
5590
  fchmod(fd, mode) {
5419
5591
  const entry = this.fdTable.get(fd);
5420
5592
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5593
+ if (entry.implicitPath) return { status: 0 };
5421
5594
  const inode = this.readInode(entry.inodeIdx);
5422
5595
  inode.mode = inode.mode & S_IFMT | mode & 4095;
5423
5596
  inode.ctime = Date.now();
@@ -5428,6 +5601,7 @@ var VFSEngine = class {
5428
5601
  fchown(fd, uid, gid) {
5429
5602
  const entry = this.fdTable.get(fd);
5430
5603
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5604
+ if (entry.implicitPath) return { status: 0 };
5431
5605
  const inode = this.readInode(entry.inodeIdx);
5432
5606
  inode.uid = uid;
5433
5607
  inode.gid = gid;
@@ -5439,6 +5613,7 @@ var VFSEngine = class {
5439
5613
  futimes(fd, atime, mtime) {
5440
5614
  const entry = this.fdTable.get(fd);
5441
5615
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5616
+ if (entry.implicitPath) return { status: 0 };
5442
5617
  const inode = this.readInode(entry.inodeIdx);
5443
5618
  inode.atime = atime;
5444
5619
  inode.mtime = mtime;
@@ -5450,7 +5625,16 @@ var VFSEngine = class {
5450
5625
  opendir(path, tabId) {
5451
5626
  path = this.normalizePath(path);
5452
5627
  const idx = this.resolvePathComponents(path, true);
5453
- 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
+ }
5454
5638
  const inode = this.readInode(idx);
5455
5639
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5456
5640
  const fd = this.nextFd++;
@@ -5489,6 +5673,106 @@ var VFSEngine = class {
5489
5673
  }
5490
5674
  return children.sort();
5491
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
+ }
5492
5776
  getAllDescendants(dirPath) {
5493
5777
  const prefix = dirPath === "/" ? "/" : dirPath + "/";
5494
5778
  const descendants = [];
@@ -5506,7 +5790,10 @@ var VFSEngine = class {
5506
5790
  if (lastSlash <= 0) return 0;
5507
5791
  const parentPath = path.substring(0, lastSlash);
5508
5792
  const parentIdx = this.pathIndex.get(parentPath);
5509
- 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
+ }
5510
5797
  const parentInode = this.readInode(parentIdx);
5511
5798
  if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
5512
5799
  return 0;