@poe-platform/safe-fs 0.1.727 → 0.1.728

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.
Files changed (57) hide show
  1. package/README.md +8 -1
  2. package/dist/safe-fs/contracts/errors.js +8 -1
  3. package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
  4. package/dist/safe-fs/contracts/io.js +183 -27
  5. package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
  6. package/dist/safe-fs/contracts/virtual-path.js +20 -1
  7. package/dist/safe-fs/core.d.ts +1 -0
  8. package/dist/safe-fs/core.js +1 -0
  9. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  10. package/dist/safe-fs/fs/capabilities.js +59 -21
  11. package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
  12. package/dist/safe-fs/fs/conditional-chmod.js +19 -0
  13. package/dist/safe-fs/fs/descriptor.js +51 -25
  14. package/dist/safe-fs/fs/devices/index.d.ts +7 -2
  15. package/dist/safe-fs/fs/devices/index.js +210 -29
  16. package/dist/safe-fs/fs/devices/path.d.ts +3 -1
  17. package/dist/safe-fs/fs/devices/path.js +70 -12
  18. package/dist/safe-fs/fs/memory/index.d.ts +34 -48
  19. package/dist/safe-fs/fs/memory/index.js +1150 -235
  20. package/dist/safe-fs/fs/memory/ledger.d.ts +2 -0
  21. package/dist/safe-fs/fs/memory/ledger.js +15 -3
  22. package/dist/safe-fs/fs/memory/missing-target.js +21 -1
  23. package/dist/safe-fs/fs/mount/index.d.ts +11 -3
  24. package/dist/safe-fs/fs/mount/index.js +326 -15
  25. package/dist/safe-fs/fs/object-publication/index.js +79 -3
  26. package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
  27. package/dist/safe-fs/fs/overlay/index.js +22 -2
  28. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
  29. package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
  30. package/dist/safe-fs/fs/quota/index.js +1 -1
  31. package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
  32. package/dist/safe-fs/fs/readonly/index.js +13 -0
  33. package/dist/safe-fs/fs/real/index.d.ts +6 -2
  34. package/dist/safe-fs/fs/real/index.js +78 -8
  35. package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
  36. package/dist/safe-fs/fs/s3/filesystem.js +48 -16
  37. package/dist/safe-fs/fs/s3/http/transport.js +20 -3
  38. package/dist/safe-fs/fs/s3/mock.js +19 -2
  39. package/dist/safe-fs/fs/s3/namespace.js +8 -2
  40. package/dist/safe-fs/fs/s3/registry.js +2 -0
  41. package/dist/safe-fs/fs/scoped.d.ts +1 -0
  42. package/dist/safe-fs/fs/scoped.js +261 -74
  43. package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
  44. package/dist/safe-fs/fs/staging-ancestry.js +115 -0
  45. package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
  46. package/dist/safe-fs/fs/staging-cleanup.js +63 -0
  47. package/dist/safe-fs/platform/browser.d.ts +5 -0
  48. package/dist/safe-fs/platform/browser.js +17 -0
  49. package/dist/safe-fs/platform/node.d.ts +5 -0
  50. package/dist/safe-fs/platform/node.js +28 -0
  51. package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
  52. package/dist/safe-fs/platform/transport-budget.js +4 -0
  53. package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
  54. package/dist/safe-fs/testing/object-io-metrics.js +29 -0
  55. package/dist/safe-fs/testing/object-publication.d.ts +3 -1
  56. package/dist/safe-fs/testing/object-publication.js +2 -2
  57. package/package.json +1 -1
@@ -1,19 +1,77 @@
1
1
  import { FsError } from "../../contracts/errors.js";
2
+ import { normalizePath } from "../../contracts/virtual-path.js";
2
3
  import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
3
4
  import { getOwnedS3Entry } from "../s3/registry.js";
4
5
  import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
5
6
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
7
  import { openFileDescriptor } from "../descriptor.js";
8
+ import { directoryAncestryPaths, runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "../staging-ancestry.js";
9
+ import { compareIdentity } from "../mount/identity.js";
10
+ import { createStagingCleanup, snapshotStagingCreation } from "../staging-cleanup.js";
7
11
  import { resolveMissingTarget } from "./missing-target.js";
8
12
  import { registerMemoryAtomicView } from "./atomic-view.js";
13
+ import { snapshotConditionalChmod } from "../conditional-chmod.js";
9
14
  import { MemoryAllocation, MemoryLedger } from "./ledger.js";
10
15
  import { normalizeMemoryFileSystemLimits } from "./limits.js";
11
16
  export { defaultMemoryFileSystemLimits } from "./limits.js";
17
+ class MemoryFileNode {
18
+ type = "file";
19
+ revision = 0;
20
+ mode;
21
+ ino;
22
+ nlink = 1;
23
+ atimeMs;
24
+ mtimeMs;
25
+ ctimeMs;
26
+ birthtimeMs;
27
+ references = 0;
28
+ byteLength;
29
+ view;
30
+ allocation;
31
+ constructor(mode, ino, now, byteLength, allocation, view) {
32
+ this.mode = mode;
33
+ this.ino = ino;
34
+ this.atimeMs = now;
35
+ this.mtimeMs = now;
36
+ this.ctimeMs = now;
37
+ this.birthtimeMs = now;
38
+ this.byteLength = byteLength;
39
+ this.allocation = allocation;
40
+ this.view = view;
41
+ }
42
+ get data() {
43
+ return this.view ??= (this.byteLength === this.allocation.data.byteLength ? this.allocation.data : this.allocation.data.subarray(0, this.byteLength));
44
+ }
45
+ set data(value) {
46
+ this.view = value;
47
+ this.byteLength = value.byteLength;
48
+ }
49
+ }
50
+ class MemoryCache {
51
+ allocations = [];
52
+ files = [];
53
+ lastFastDirPrefix = "";
54
+ lastFastDirNode;
55
+ lastFastFilePath = "";
56
+ lastFastFileNode;
57
+ clearWrites() {
58
+ this.lastFastDirPrefix = "";
59
+ this.lastFastDirNode = undefined;
60
+ this.lastFastFilePath = "";
61
+ this.lastFastFileNode = undefined;
62
+ }
63
+ }
12
64
  const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
65
+ const emptyResolveOptions = Object.freeze({});
66
+ const noFollowResolveOptions = Object.freeze({ followFinal: false });
67
+ const resolvedVoid = Promise.resolve();
13
68
  const preferredIoBlockSize = 64 * 1024;
14
69
  const ext4HtreeEof64 = (1n << 63n) - 1n;
70
+ const extractionStreamGuard = Symbol("extractionStreamGuard");
15
71
  const ownedStats = new WeakMap();
16
72
  const ownedStores = new WeakMap();
73
+ // Forwarded receivers share the ledger, so mutations invalidate the owner's cache.
74
+ const memoryCaches = new WeakMap();
17
75
  const registeredAuthorities = new WeakSet();
18
76
  const compareOwnedMemory = async (own, peer, options) => {
19
77
  options.signal?.throwIfAborted();
@@ -55,7 +113,22 @@ const compareOwnedMemory = async (own, peer, options) => {
55
113
  options.signal?.throwIfAborted();
56
114
  return qualified ? "distinct" : "unknown";
57
115
  };
116
+ const activeConditionalMutations = new Map();
117
+ export async function bindConditionalMutation(identityScope, binding, action) {
118
+ if (identityScope === undefined || identityScope === null)
119
+ return action();
120
+ activeConditionalMutations.set(identityScope, binding);
121
+ try {
122
+ return await action();
123
+ }
124
+ finally {
125
+ if (activeConditionalMutations.get(identityScope) === binding) {
126
+ activeConditionalMutations.delete(identityScope);
127
+ }
128
+ }
129
+ }
58
130
  export class MemoryFileSystem {
131
+ capabilitiesFor;
59
132
  capabilities = ((filesystem) => {
60
133
  return Object.freeze({
61
134
  read: true, stat: true, readdir: true, realpath: true, access: true, open: true,
@@ -64,12 +137,13 @@ export class MemoryFileSystem {
64
137
  rename: true, atomicRenameNoReplace: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
65
138
  streamingAppend: true, randomAccessWrite: true,
66
139
  readOnly: false,
67
- symlinks: true,
140
+ symlinks: true, conditionalChmod: true,
68
141
  hardlinks: true,
69
142
  permissions: true,
70
143
  timestamps: true,
71
144
  atomicRename: true,
72
- atomicFileStaging: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicTreeRemoval: true,
145
+ atomicFileStaging: true, retainedStagingCleanup: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
146
+ synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
73
147
  atomicDirectoryMetadata: true,
74
148
  streamingRead: true,
75
149
  retainedRead: true,
@@ -83,8 +157,10 @@ export class MemoryFileSystem {
83
157
  ledger;
84
158
  root;
85
159
  totalBytes = 0;
160
+ symlinkCount = 0;
86
161
  constructor(options = {}) {
87
162
  this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
163
+ memoryCaches.set(this.ledger, new MemoryCache());
88
164
  this.ledger.reserve(0, 1, "mkdir", "/");
89
165
  this.root = this.directory(0o755);
90
166
  const root = this.root;
@@ -117,16 +193,28 @@ export class MemoryFileSystem {
117
193
  return [...node.entries.keys()];
118
194
  },
119
195
  }, () => {
120
- if (Object.getPrototypeOf(this) !== MemoryFileSystem.prototype
121
- || Object.getOwnPropertyDescriptor(this, "root")?.value !== root
122
- || Object.getOwnPropertyDescriptor(this, "ledger")?.value !== ownedStores.get(this)?.ledger
123
- || Object.getOwnPropertyDescriptor(this, "capabilities")?.value !== ownedStores.get(this)?.capabilities)
196
+ if (Object.getPrototypeOf(this) !== MemoryFileSystem.prototype ||
197
+ this.root !== root ||
198
+ this.ledger !== ownedStores.get(this)?.ledger ||
199
+ this.capabilities !== ownedStores.get(this)?.capabilities) {
124
200
  return false;
125
- return Object.entries(memoryImplementation).every(([name, expected]) => {
126
- const actual = Object.getOwnPropertyDescriptor(this, name)
127
- ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
128
- return actual?.value === expected.value && actual?.get === expected.get && actual?.set === expected.set;
129
- });
201
+ }
202
+ const proto = MemoryFileSystem.prototype;
203
+ for (let i = 0; i < memoryImplementationKeys.length; i++) {
204
+ const name = memoryImplementationKeys[i];
205
+ const expected = memoryImplementationDescriptors[i];
206
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
207
+ const actual = Object.getOwnPropertyDescriptor(this, name);
208
+ if (!actual || actual.value !== expected.value || actual.get !== expected.get || actual.set !== expected.set)
209
+ return false;
210
+ }
211
+ else if (expected.get !== undefined || expected.set !== undefined || proto[name] !== expected.value) {
212
+ const actual = Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
213
+ if (!actual || actual.value !== expected.value || actual.get !== expected.get || actual.set !== expected.set)
214
+ return false;
215
+ }
216
+ }
217
+ return true;
130
218
  });
131
219
  if (this.compareEntry === memoryImplementation.compareEntry?.value) {
132
220
  registeredAuthorities.add(this);
@@ -138,16 +226,9 @@ export class MemoryFileSystem {
138
226
  }
139
227
  canonicalizeMissingTarget(path, options = {}) {
140
228
  options.signal?.throwIfAborted();
141
- const owner = ownedStores.get(this);
142
- if (!owner || Object.getPrototypeOf(this) !== MemoryFileSystem.prototype
143
- || Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root)
229
+ if (!isStockMemoryMethods(this, missingTargetMethodNames))
144
230
  return undefined;
145
- for (const name of ["realpath", "lstat", "resolve", "permission", "validatePath", "fail", "snapshot"]) {
146
- const descriptor = Object.getOwnPropertyDescriptor(this, name)
147
- ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
148
- if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
149
- return undefined;
150
- }
231
+ const owner = ownedStores.get(this);
151
232
  if (path !== "")
152
233
  this.validatePath(path, "realpath");
153
234
  return resolveMissingTarget(owner.root, path || ".", options.signal);
@@ -169,6 +250,7 @@ export class MemoryFileSystem {
169
250
  }
170
251
  retained.set("/", this.root);
171
252
  }
253
+ const capabilities = Object.freeze({ ...this.capabilities, retainedStagingCleanup: false });
172
254
  const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional"]);
173
255
  const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
174
256
  const check = (path, followFinal) => {
@@ -193,6 +275,8 @@ export class MemoryFileSystem {
193
275
  get: (target, property) => {
194
276
  if (property === "objects" || property === "confineExtraction")
195
277
  return undefined;
278
+ if (property === "capabilities")
279
+ return capabilities;
196
280
  const value = Reflect.get(target, property);
197
281
  if (typeof value !== "function")
198
282
  return value;
@@ -201,14 +285,22 @@ export class MemoryFileSystem {
201
285
  return (...args) => {
202
286
  if (!allowed.has(String(property)))
203
287
  throw new FsError("ENOTSUP", { syscall: String(property) });
204
- // Stock mutations resolve and commit synchronously; writeStream retains
205
- // its file node before its first await, so later chunks cannot redirect.
288
+ // Stock mutations resolve and commit synchronously. Streamed writes
289
+ // repeat the boundary and file-binding check at each backend mutation.
206
290
  const paths = property === "symlink" ? [args[1]] : property === "link" || property === "rename" ? args.slice(0, 2) : [args[0]];
207
291
  for (const path of paths) {
208
292
  if (typeof path !== "string")
209
293
  throw new FsError("EINVAL");
210
294
  check(path, ["chmod", "utimes", "appendFile", "writeFile", "writeStream", "writeFileConditional", "link"].includes(String(property)));
211
295
  }
296
+ if (property === "writeStream") {
297
+ const path = args[0];
298
+ args[2] = { ...args[2], [extractionStreamGuard]: (node) => {
299
+ check(path, true);
300
+ if (this.resolve(path, "writeStream").node !== node)
301
+ this.fail("EPERM", "writeStream", path);
302
+ } };
303
+ }
212
304
  return Reflect.apply(value, target, args);
213
305
  };
214
306
  },
@@ -230,10 +322,12 @@ export class MemoryFileSystem {
230
322
  try {
231
323
  const node = create();
232
324
  if (node.type === "file")
233
- this.admitSize(undefined, node.data.byteLength, syscall, path);
325
+ this.admitSize(undefined, node.byteLength, syscall, path);
234
326
  parent.entries.set(name, node);
235
327
  if (node.type === "file")
236
- this.totalBytes += node.data.byteLength;
328
+ this.totalBytes += node.byteLength;
329
+ else if (node.type === "symlink")
330
+ this.symlinkCount++;
237
331
  this.changed(parent);
238
332
  return node;
239
333
  }
@@ -245,10 +339,30 @@ export class MemoryFileSystem {
245
339
  releaseNode(node) {
246
340
  if (node.nlink !== 0 || node.references !== 0)
247
341
  return;
342
+ const cache = memoryCaches.get(this.ledger);
248
343
  this.ledger.release(node.type === "symlink" ? node.target.length * 2 : 0, 1);
249
344
  if (node.type === "file") {
250
- this.totalBytes -= node.data.byteLength;
251
- node.allocation.release();
345
+ if (cache.lastFastFileNode === node) {
346
+ cache.lastFastFilePath = "";
347
+ cache.lastFastFileNode = undefined;
348
+ }
349
+ this.totalBytes -= node.byteLength;
350
+ const alloc = node.allocation;
351
+ node.view = undefined;
352
+ alloc.release();
353
+ if (alloc.isReleased64() && cache.allocations.length < 128) {
354
+ cache.allocations.push(alloc);
355
+ }
356
+ if (cache.files.length < 128) {
357
+ cache.files.push(node);
358
+ }
359
+ }
360
+ else if (node.type === "symlink") {
361
+ this.symlinkCount--;
362
+ }
363
+ else if (cache.lastFastDirNode === node) {
364
+ cache.lastFastDirPrefix = "";
365
+ cache.lastFastDirNode = undefined;
252
366
  }
253
367
  }
254
368
  releaseReference(node, path) {
@@ -258,14 +372,16 @@ export class MemoryFileSystem {
258
372
  }
259
373
  replaceData(node, allocation, length = allocation.data.byteLength) {
260
374
  const previous = node.allocation;
261
- this.totalBytes += length - node.data.byteLength;
262
- node.data = allocation.data.subarray(0, length);
375
+ this.totalBytes += length - node.byteLength;
376
+ node.byteLength = length;
377
+ node.view = length === allocation.data.byteLength ? allocation.data : undefined;
263
378
  node.allocation = allocation;
264
379
  previous.release();
265
380
  }
266
381
  admitSize(node, length, syscall, path) {
267
382
  this.ledger.fileSize(length, syscall, path);
268
- if (length - (node?.data.byteLength ?? 0) > (this.ledger.limits.maxBytes ?? Number.MAX_SAFE_INTEGER) - this.totalBytes) {
383
+ const maxBytes = this.ledger.limits.maxBytes;
384
+ if (maxBytes !== undefined && length - (node?.byteLength ?? 0) > maxBytes - this.totalBytes) {
269
385
  this.fail("ENOSPC", syscall, path);
270
386
  }
271
387
  }
@@ -293,19 +409,117 @@ export class MemoryFileSystem {
293
409
  this.fail("EINVAL", syscall, path);
294
410
  }
295
411
  changed(node) {
296
- node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
412
+ node.revision = node.revision < 1073741823 ? (node.revision + 1) | 0 : Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
297
413
  node.mtimeMs = node.ctimeMs = Date.now();
298
414
  }
299
- resolve(path, syscall, options = {}) {
415
+ resolveNode(path, syscall, followFinal = true) {
416
+ const cache = memoryCaches.get(this.ledger);
417
+ if (this.symlinkCount === 0 && isCleanAbsolutePath(path)) {
418
+ const fastDir = cache.lastFastDirNode;
419
+ const fastPrefix = cache.lastFastDirPrefix;
420
+ if (fastDir !== undefined && fastDir.nlink !== 0 && fastPrefix.length > 0 && path.startsWith(fastPrefix)) {
421
+ const name = path.slice(fastPrefix.length);
422
+ if (name.length > 0 && !name.includes("/")) {
423
+ this.permission(fastDir, 1, syscall, path);
424
+ if (exceedsComponentByteLimit(name))
425
+ this.fail("ENAMETOOLONG", syscall, path);
426
+ const node = fastDir.entries.get(name);
427
+ if (!node)
428
+ this.fail("ENOENT", syscall, path);
429
+ return node;
430
+ }
431
+ }
432
+ let current = this.root;
433
+ let start = 1;
434
+ while (true) {
435
+ this.permission(current, 1, syscall, path);
436
+ const slash = path.indexOf("/", start);
437
+ if (slash === -1) {
438
+ cache.clearWrites();
439
+ cache.lastFastDirPrefix = path.slice(0, start);
440
+ cache.lastFastDirNode = current;
441
+ const name = path.slice(start);
442
+ if (exceedsComponentByteLimit(name))
443
+ this.fail("ENAMETOOLONG", syscall, path);
444
+ const node = current.entries.get(name);
445
+ if (!node)
446
+ this.fail("ENOENT", syscall, path);
447
+ return node;
448
+ }
449
+ const name = path.slice(start, slash);
450
+ if (exceedsComponentByteLimit(name))
451
+ this.fail("ENAMETOOLONG", syscall, path);
452
+ const next = current.entries.get(name);
453
+ if (!next)
454
+ this.fail("ENOENT", syscall, path);
455
+ if (next.type !== "directory")
456
+ this.fail("ENOTDIR", syscall, path);
457
+ current = next;
458
+ start = slash + 1;
459
+ }
460
+ }
461
+ return this.resolve(path, syscall, followFinal ? emptyResolveOptions : noFollowResolveOptions).node;
462
+ }
463
+ resolve(path, syscall, options = emptyResolveOptions) {
300
464
  this.validatePath(path, syscall);
465
+ if (this.symlinkCount === 0 && options.createDirectories === undefined && options.resizeCreate === undefined
466
+ && options.resolutionSteps === undefined && isCleanAbsolutePath(path)) {
467
+ let current = this.root;
468
+ let start = 1;
469
+ while (true) {
470
+ this.permission(current, 1, syscall, path);
471
+ const slash = path.indexOf("/", start);
472
+ if (slash === -1) {
473
+ const name = path.slice(start);
474
+ if (exceedsComponentByteLimit(name))
475
+ this.fail("ENAMETOOLONG", syscall, path);
476
+ const node = current.entries.get(name);
477
+ if (!node && !options.allowMissing)
478
+ this.fail("ENOENT", syscall, path);
479
+ return { node, parent: current, name, path };
480
+ }
481
+ const name = path.slice(start, slash);
482
+ if (exceedsComponentByteLimit(name))
483
+ this.fail("ENAMETOOLONG", syscall, path);
484
+ const next = current.entries.get(name);
485
+ if (!next)
486
+ this.fail("ENOENT", syscall, path);
487
+ if (next.type !== "directory")
488
+ this.fail("ENOTDIR", syscall, path);
489
+ current = next;
490
+ start = slash + 1;
491
+ }
492
+ }
301
493
  // Bound all component arrays allocated by this resolution, including cycles.
302
494
  let remainingPathUnits = 65_536 - path.length;
303
495
  if (remainingPathUnits < 0)
304
496
  this.fail("ENAMETOOLONG", syscall, path);
497
+ let initialComponents = 0;
498
+ let inComp = false;
499
+ for (let i = 0; i < path.length; i++) {
500
+ if (path.charCodeAt(i) === 47)
501
+ inComp = false;
502
+ else if (!inComp) {
503
+ inComp = true;
504
+ if (++initialComponents > 256)
505
+ this.fail("ENAMETOOLONG", syscall, path);
506
+ }
507
+ }
305
508
  const pending = path.split("/").filter(Boolean);
509
+ let remainingComponents = 256 - pending.length;
306
510
  if (path.endsWith("/"))
307
511
  pending.push("");
308
512
  const stack = [{ node: this.root, name: "" }];
513
+ let recordedUnits = 0;
514
+ const observe = (node, entryPath) => {
515
+ if (!options.resolutionSteps)
516
+ return;
517
+ recordedUnits += entryPath.length + (node.type === "symlink" ? node.target.length : 0);
518
+ if (options.resolutionSteps.length >= 4096 || recordedUnits > 1_048_576)
519
+ this.fail("EFBIG", syscall, path);
520
+ options.resolutionSteps.push({ path: entryPath, stat: this.snapshot(node), ...(node.type === "symlink" ? { linkTarget: node.target } : {}) });
521
+ };
522
+ observe(this.root, "/");
309
523
  let links = 0;
310
524
  while (pending.length > 0) {
311
525
  const component = pending.shift();
@@ -321,7 +535,7 @@ export class MemoryFileSystem {
321
535
  stack.pop();
322
536
  continue;
323
537
  }
324
- if (new TextEncoder().encode(component).byteLength > 255)
538
+ if (exceedsComponentByteLimit(component))
325
539
  this.fail("ENAMETOOLONG", syscall, path);
326
540
  if (options.resizeCreate === true && pending.length === 1 && pending[0] === "")
327
541
  this.fail("EISDIR", syscall, path);
@@ -337,6 +551,8 @@ export class MemoryFileSystem {
337
551
  }
338
552
  this.fail("ENOENT", syscall, path);
339
553
  }
554
+ if (options.resolutionSteps)
555
+ observe(node, [...stack.map(entry => entry.name), component].join("/"));
340
556
  if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
341
557
  if (++links > 40)
342
558
  this.fail("ELOOP", syscall, path);
@@ -344,6 +560,9 @@ export class MemoryFileSystem {
344
560
  this.fail("ENAMETOOLONG", syscall, path);
345
561
  remainingPathUnits -= node.target.length;
346
562
  const target = node.target.split("/").filter(Boolean);
563
+ if (target.length > remainingComponents)
564
+ this.fail("ENAMETOOLONG", syscall, path);
565
+ remainingComponents -= target.length;
347
566
  if (node.target.endsWith("/"))
348
567
  target.push("");
349
568
  if (options.resizeCreate !== undefined && target.at(-1) === "" && pending[0] === "")
@@ -364,6 +583,55 @@ export class MemoryFileSystem {
364
583
  };
365
584
  }
366
585
  file(path, syscall) {
586
+ const cache = memoryCaches.get(this.ledger);
587
+ if (this.symlinkCount === 0 && isCleanAbsolutePath(path)) {
588
+ const fastDir = cache.lastFastDirNode;
589
+ const fastPrefix = cache.lastFastDirPrefix;
590
+ if (fastDir !== undefined && fastDir.nlink !== 0 && fastPrefix.length > 0 && path.startsWith(fastPrefix)) {
591
+ const name = path.slice(fastPrefix.length);
592
+ if (name.length > 0 && !name.includes("/")) {
593
+ this.permission(fastDir, 1, syscall, path);
594
+ if (exceedsComponentByteLimit(name))
595
+ this.fail("ENAMETOOLONG", syscall, path);
596
+ const node = fastDir.entries.get(name);
597
+ if (!node)
598
+ this.fail("ENOENT", syscall, path);
599
+ if (node.type !== "file")
600
+ this.fail("EISDIR", syscall, path);
601
+ return node;
602
+ }
603
+ }
604
+ let current = this.root;
605
+ let start = 1;
606
+ while (true) {
607
+ this.permission(current, 1, syscall, path);
608
+ const slash = path.indexOf("/", start);
609
+ if (slash === -1) {
610
+ cache.clearWrites();
611
+ cache.lastFastDirPrefix = path.slice(0, start);
612
+ cache.lastFastDirNode = current;
613
+ const name = path.slice(start);
614
+ if (exceedsComponentByteLimit(name))
615
+ this.fail("ENAMETOOLONG", syscall, path);
616
+ const node = current.entries.get(name);
617
+ if (!node)
618
+ this.fail("ENOENT", syscall, path);
619
+ if (node.type !== "file")
620
+ this.fail("EISDIR", syscall, path);
621
+ return node;
622
+ }
623
+ const name = path.slice(start, slash);
624
+ if (exceedsComponentByteLimit(name))
625
+ this.fail("ENAMETOOLONG", syscall, path);
626
+ const next = current.entries.get(name);
627
+ if (!next)
628
+ this.fail("ENOENT", syscall, path);
629
+ if (next.type !== "directory")
630
+ this.fail("ENOTDIR", syscall, path);
631
+ current = next;
632
+ start = slash + 1;
633
+ }
634
+ }
367
635
  const node = this.resolve(path, syscall).node;
368
636
  if (node.type !== "file")
369
637
  this.fail("EISDIR", syscall, path);
@@ -379,19 +647,58 @@ export class MemoryFileSystem {
379
647
  terminalDot(path) {
380
648
  return /(?:^|\/)\.{1,2}\/*$/.test(path);
381
649
  }
650
+ directoryNlink(node) {
651
+ if (node.cachedNlinkRev === node.revision && node.cachedNlink !== undefined) {
652
+ return node.cachedNlink;
653
+ }
654
+ let nlink = 2;
655
+ node.entries.forEach(entry => {
656
+ if (entry.type === "directory")
657
+ nlink++;
658
+ });
659
+ node.cachedNlinkRev = node.revision;
660
+ node.cachedNlink = nlink;
661
+ return nlink;
662
+ }
382
663
  snapshot(node) {
664
+ const hasRev = Number.isSafeInteger(node.revision);
665
+ const hasScope = ownedStores.get(this)?.intact() === true;
666
+ const size = node.type === "file" ? node.byteLength
667
+ : node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0;
668
+ const nlink = node.type === "directory" && node.nlink !== 0
669
+ ? this.directoryNlink(node) : node.nlink;
670
+ if (hasRev && hasScope) {
671
+ return {
672
+ type: node.type,
673
+ filesystemType: "memory",
674
+ ioBlockSize: preferredIoBlockSize,
675
+ revision: node.revision,
676
+ preferredIoBlockSize: 4096,
677
+ size,
678
+ mode: node.mode,
679
+ identityScope: this.identityScope,
680
+ ino: node.ino,
681
+ dev: 0,
682
+ uid: 0,
683
+ gid: 0,
684
+ nlink,
685
+ atimeMs: node.atimeMs,
686
+ mtimeMs: node.mtimeMs,
687
+ ctimeMs: node.ctimeMs,
688
+ birthtimeMs: node.birthtimeMs,
689
+ };
690
+ }
383
691
  return {
384
692
  type: node.type,
385
693
  filesystemType: "memory",
386
694
  ioBlockSize: preferredIoBlockSize,
387
- ...(Number.isSafeInteger(node.revision) ? { revision: node.revision } : {}),
695
+ ...(hasRev ? { revision: node.revision } : {}),
388
696
  preferredIoBlockSize: 4096,
389
- size: node.type === "file" ? node.data.byteLength
390
- : node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0,
391
- mode: node.mode, ...(ownedStores.get(this)?.intact() ? { identityScope: this.identityScope } : {}),
697
+ size,
698
+ mode: node.mode,
699
+ ...(hasScope ? { identityScope: this.identityScope } : {}),
392
700
  ino: node.ino, dev: 0, uid: 0, gid: 0,
393
- nlink: node.type === "directory" && node.nlink !== 0
394
- ? 2 + [...node.entries.values()].filter((entry) => entry.type === "directory").length : node.nlink,
701
+ nlink,
395
702
  atimeMs: node.atimeMs, mtimeMs: node.mtimeMs, ctimeMs: node.ctimeMs, birthtimeMs: node.birthtimeMs,
396
703
  };
397
704
  }
@@ -409,6 +716,12 @@ export class MemoryFileSystem {
409
716
  allocate(length, syscall, path) {
410
717
  this.ledger.fileSize(length, syscall, path);
411
718
  this.ledger.reserve(length, 0, syscall, path);
719
+ const allocations = memoryCaches.get(this.ledger).allocations;
720
+ if (length === 64 && allocations.length > 0) {
721
+ const pooled = allocations.pop();
722
+ pooled.reuse();
723
+ return pooled;
724
+ }
412
725
  try {
413
726
  return new MemoryAllocation(new Uint8Array(length), this.ledger);
414
727
  }
@@ -418,9 +731,7 @@ export class MemoryFileSystem {
418
731
  }
419
732
  }
420
733
  open(path, options) {
421
- return openFileDescriptor(path, options, {
422
- noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
423
- }, async (admitted) => {
734
+ return openFileDescriptor(path, options, MEMORY_DESCRIPTOR_CAPABILITIES, async (admitted) => {
424
735
  const location = this.resolve(path, "open", {
425
736
  followFinal: !admitted.noFollow && admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
426
737
  });
@@ -448,40 +759,16 @@ export class MemoryFileSystem {
448
759
  throw error;
449
760
  }
450
761
  node.references++;
451
- const resource = { node, position: 0 };
762
+ const resource = { fs: this, path, append: admitted.append, node, position: 0 };
452
763
  return {
453
764
  resource,
454
- getPosition: async (retained) => retained.position,
455
- stat: async (retained) => this.snapshot(retained.node),
456
- read: async (retained, buffer, position) => {
457
- const inode = retained.node;
458
- const start = position ?? retained.position;
459
- const count = Math.min(buffer.byteLength, Math.max(0, inode.data.byteLength - start));
460
- buffer.set(inode.data.subarray(start, start + count));
461
- if (position === null)
462
- retained.position += count;
463
- inode.atimeMs = Date.now();
464
- return count;
465
- },
466
- write: async (retained, buffer, position) => {
467
- const inode = retained.node;
468
- const start = admitted.append ? inode.data.byteLength : position ?? retained.position;
469
- const end = start + buffer.byteLength;
470
- this.writeAt(inode, buffer, start, "write", path);
471
- if (position === null)
472
- retained.position = end;
473
- return buffer.byteLength;
474
- },
475
- truncate: async (retained, length) => {
476
- const inode = retained.node;
477
- this.resizeNode(inode, length, "ftruncate", path);
478
- },
479
- sync: async () => { },
480
- close: async (retained) => {
481
- const inode = retained.node;
482
- retained.node = undefined;
483
- this.releaseReference(inode, path);
484
- },
765
+ getPosition: memoryDescriptorGetPosition,
766
+ stat: memoryDescriptorStat,
767
+ read: memoryDescriptorRead,
768
+ write: memoryDescriptorWrite,
769
+ truncate: memoryDescriptorTruncate,
770
+ sync: memoryDescriptorSync,
771
+ close: memoryDescriptorClose,
485
772
  };
486
773
  });
487
774
  }
@@ -510,7 +797,8 @@ export class MemoryFileSystem {
510
797
  return location.node;
511
798
  return this.addNode(location.parent, location.name, () => {
512
799
  const allocation = new MemoryAllocation(new Uint8Array(), this.ledger);
513
- return { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation };
800
+ const meta = this.metadata(typeModes.file | mode);
801
+ return new MemoryFileNode(meta.mode, meta.ino, meta.atimeMs, 0, allocation, allocation.data);
514
802
  }, syscall, path);
515
803
  }
516
804
  writeData(path, data, options, syscall) {
@@ -518,41 +806,58 @@ export class MemoryFileSystem {
518
806
  throw new TypeError("Memory files require Uint8Array data");
519
807
  const target = this.prepareWrite(path, options, syscall);
520
808
  const current = target.location.node;
521
- const length = (target.append ? current?.data.byteLength ?? 0 : 0) + data.byteLength;
809
+ const length = (target.append ? current?.byteLength ?? 0 : 0) + data.byteLength;
522
810
  this.admitSize(current, length, syscall, path);
523
811
  const growth = target.append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
524
812
  this.ledger.check(data.byteLength + growth + (current ? 0 : target.location.name.length * 2), current ? 0 : 2, syscall, path);
525
- const copied = this.bytes(data, syscall, path);
526
- let allocation = copied;
527
- let transferred = false;
528
- try {
529
- if (!current) {
530
- if (target.append && length > 0) {
531
- const capacity = Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - target.location.name.length * 2);
532
- allocation = this.allocate(capacity, syscall, path);
533
- allocation.data.set(copied.data);
534
- }
535
- const node = this.addNode(target.location.parent, target.location.name, () => ({
536
- ...this.metadata(typeModes.file | target.mode), type: "file",
537
- data: allocation.data.subarray(0, length), allocation,
538
- }), syscall, path);
539
- transferred = true;
540
- return node;
813
+ if (current && target.append) {
814
+ this.writeAt(current, data, current.byteLength, syscall, path);
815
+ return current;
816
+ }
817
+ if (!current) {
818
+ const capacity = length > 0 && (target.append || (length < 64 && this.ledger.availableBytes > 65536 && this.ledger.limits.maxFileBytes >= 64))
819
+ ? Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - target.location.name.length * 2)
820
+ : length;
821
+ const allocation = this.allocate(capacity, syscall, path);
822
+ try {
823
+ allocation.data.set(data);
824
+ return this.addNode(target.location.parent, target.location.name, () => {
825
+ const now = Date.now();
826
+ const fileMode = typeModes.file | target.mode;
827
+ const view = capacity === length ? allocation.data : undefined;
828
+ const pooled = memoryCaches.get(this.ledger).files.pop();
829
+ if (pooled) {
830
+ pooled.mode = fileMode;
831
+ pooled.ino = this.nextInode++;
832
+ pooled.nlink = 1;
833
+ pooled.references = 0;
834
+ pooled.revision = 0;
835
+ pooled.atimeMs = now;
836
+ pooled.mtimeMs = now;
837
+ pooled.ctimeMs = now;
838
+ pooled.birthtimeMs = now;
839
+ pooled.byteLength = length;
840
+ pooled.view = view;
841
+ pooled.allocation = allocation;
842
+ return pooled;
843
+ }
844
+ return new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view);
845
+ }, syscall, path);
541
846
  }
542
- if (target.append)
543
- this.writeAt(current, copied.data, current.data.byteLength, syscall, path);
544
- else {
545
- this.replaceData(current, copied);
546
- transferred = true;
547
- this.changed(current);
847
+ catch (error) {
848
+ allocation.release();
849
+ throw error;
548
850
  }
851
+ }
852
+ const copied = this.bytes(data, syscall, path);
853
+ try {
854
+ this.replaceData(current, copied);
855
+ this.changed(current);
549
856
  return current;
550
857
  }
551
- finally {
552
- if (allocation !== copied && !transferred)
553
- allocation.release();
554
- if (!transferred || allocation !== copied)
555
- copied.release();
858
+ catch (error) {
859
+ copied.release();
860
+ throw error;
556
861
  }
557
862
  }
558
863
  writeAt(node, data, position, syscall, path) {
@@ -562,20 +867,21 @@ export class MemoryFileSystem {
562
867
  }
563
868
  const end = position + data.byteLength;
564
869
  this.ledger.fileSize(end, syscall, path);
565
- const length = Math.max(node.data.byteLength, end);
870
+ const curLen = node.byteLength;
871
+ const length = Math.max(curLen, end);
566
872
  this.admitSize(node, length, syscall, path);
567
873
  let allocation = node.allocation;
568
874
  if (length > allocation.data.byteLength) {
569
875
  this.ledger.check(length, 0, syscall, path);
570
- const capacity = Math.min(Math.max(length, node.data.byteLength * 2, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes);
876
+ const capacity = Math.min(Math.max(length, curLen * 2, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes);
571
877
  allocation = this.allocate(capacity, syscall, path);
572
878
  }
573
879
  try {
574
880
  const storage = allocation.data;
575
881
  if (allocation !== node.allocation)
576
- storage.set(node.data);
577
- if (position > node.data.byteLength)
578
- storage.fill(0, node.data.byteLength, position);
882
+ storage.set(node.view ?? (curLen === node.allocation.data.byteLength ? node.allocation.data : node.allocation.data.subarray(0, curLen)));
883
+ if (position > curLen)
884
+ storage.fill(0, curLen, position);
579
885
  storage.set(data, position);
580
886
  }
581
887
  catch (error) {
@@ -586,11 +892,157 @@ export class MemoryFileSystem {
586
892
  if (allocation !== node.allocation)
587
893
  this.replaceData(node, allocation, length);
588
894
  else {
589
- this.totalBytes += length - node.data.byteLength;
590
- node.data = allocation.data.subarray(0, length);
895
+ this.totalBytes += length - curLen;
896
+ node.byteLength = length;
897
+ node.view = length === allocation.data.byteLength ? allocation.data : undefined;
591
898
  }
592
899
  this.changed(node);
593
900
  }
901
+ writeMemoryFileFast(path, data, append, mode) {
902
+ const cache = memoryCaches.get(this.ledger);
903
+ const syscall = append ? "appendFile" : "writeFile";
904
+ if (append &&
905
+ path === cache.lastFastFilePath &&
906
+ cache.lastFastFileNode !== undefined &&
907
+ cache.lastFastFileNode.nlink !== 0 &&
908
+ cache.lastFastDirNode !== undefined &&
909
+ cache.lastFastDirNode.nlink !== 0 &&
910
+ (cache.lastFastDirNode.mode & 1) !== 0 &&
911
+ (cache.lastFastFileNode.mode & 2) !== 0) {
912
+ const current = cache.lastFastFileNode;
913
+ const length = current.byteLength + data.byteLength;
914
+ this.admitSize(current, length, syscall, path);
915
+ const growth = length > current.allocation.data.byteLength ? length : 0;
916
+ this.ledger.check(data.byteLength + growth, 0, syscall, path);
917
+ this.writeAt(current, data, current.byteLength, syscall, path);
918
+ return;
919
+ }
920
+ let parent = this.root;
921
+ let start = 1;
922
+ let name = "";
923
+ const cachedPrefix = cache.lastFastDirPrefix;
924
+ const cachedDir = cache.lastFastDirNode;
925
+ if (cachedDir !== undefined &&
926
+ cachedDir.nlink !== 0 &&
927
+ (cachedDir.mode & 1) !== 0 &&
928
+ cachedPrefix.length > 1 &&
929
+ path.startsWith(cachedPrefix) &&
930
+ path.indexOf("/", cachedPrefix.length) === -1) {
931
+ parent = cachedDir;
932
+ name = path.slice(cachedPrefix.length);
933
+ if (exceedsComponentByteLimit(name))
934
+ this.fail("ENAMETOOLONG", syscall, path);
935
+ }
936
+ else {
937
+ while (true) {
938
+ this.permission(parent, 1, syscall, path);
939
+ const slash = path.indexOf("/", start);
940
+ if (slash === -1) {
941
+ name = path.slice(start);
942
+ if (exceedsComponentByteLimit(name))
943
+ this.fail("ENAMETOOLONG", syscall, path);
944
+ if (start > 1 && (parent.mode & 1) !== 0) {
945
+ cache.lastFastDirPrefix = path.slice(0, start);
946
+ cache.lastFastDirNode = parent;
947
+ }
948
+ break;
949
+ }
950
+ const seg = path.slice(start, slash);
951
+ if (exceedsComponentByteLimit(seg))
952
+ this.fail("ENAMETOOLONG", syscall, path);
953
+ const next = parent.entries.get(seg);
954
+ if (!next)
955
+ this.fail("ENOENT", syscall, path);
956
+ if (next.type !== "directory")
957
+ this.fail("ENOTDIR", syscall, path);
958
+ parent = next;
959
+ start = slash + 1;
960
+ }
961
+ }
962
+ const existing = parent.entries.get(name);
963
+ if (existing) {
964
+ if (existing.type !== "file")
965
+ this.fail("EISDIR", syscall, path);
966
+ this.permission(existing, 2, syscall, path);
967
+ }
968
+ else {
969
+ this.permission(parent, 3, syscall, path);
970
+ }
971
+ const current = existing;
972
+ const length = (append ? current?.byteLength ?? 0 : 0) + data.byteLength;
973
+ this.admitSize(current, length, syscall, path);
974
+ const growth = append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
975
+ const nameBytes = name.length * 2;
976
+ this.ledger.check(data.byteLength + growth + (current ? 0 : nameBytes), current ? 0 : 2, syscall, path);
977
+ if (current && append) {
978
+ this.writeAt(current, data, current.byteLength, syscall, path);
979
+ cache.lastFastFilePath = path;
980
+ cache.lastFastFileNode = current;
981
+ return;
982
+ }
983
+ if (!current) {
984
+ const capacity = length > 0 && (append || (length < 64 && this.ledger.availableBytes > 65536 && this.ledger.limits.maxFileBytes >= 64))
985
+ ? Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - nameBytes)
986
+ : length;
987
+ const allocation = this.allocate(capacity, syscall, path);
988
+ try {
989
+ allocation.data.set(data);
990
+ this.ledger.reserve(nameBytes, 2, syscall, path);
991
+ try {
992
+ const now = Date.now();
993
+ const fileMode = typeModes.file | mode;
994
+ const view = capacity === length ? allocation.data : undefined;
995
+ let node = cache.files.pop();
996
+ if (node) {
997
+ node.mode = fileMode;
998
+ node.ino = this.nextInode++;
999
+ node.nlink = 1;
1000
+ node.references = 0;
1001
+ node.revision = 0;
1002
+ node.atimeMs = now;
1003
+ node.mtimeMs = now;
1004
+ node.ctimeMs = now;
1005
+ node.birthtimeMs = now;
1006
+ node.byteLength = length;
1007
+ node.view = view;
1008
+ node.allocation = allocation;
1009
+ }
1010
+ else {
1011
+ node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view);
1012
+ }
1013
+ this.admitSize(undefined, length, syscall, path);
1014
+ const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
1015
+ parent.entries.set(name, node);
1016
+ this.totalBytes += length;
1017
+ this.changed(parent);
1018
+ if (prevNlink !== undefined) {
1019
+ parent.cachedNlink = prevNlink;
1020
+ parent.cachedNlinkRev = parent.revision;
1021
+ }
1022
+ cache.lastFastFilePath = path;
1023
+ cache.lastFastFileNode = node;
1024
+ }
1025
+ catch (error) {
1026
+ this.ledger.release(nameBytes, 2);
1027
+ throw error;
1028
+ }
1029
+ }
1030
+ catch (error) {
1031
+ allocation.release();
1032
+ throw error;
1033
+ }
1034
+ return;
1035
+ }
1036
+ const copied = this.bytes(data, syscall, path);
1037
+ try {
1038
+ this.replaceData(current, copied);
1039
+ this.changed(current);
1040
+ }
1041
+ catch (error) {
1042
+ copied.release();
1043
+ throw error;
1044
+ }
1045
+ }
594
1046
  async readFile(path, options = {}) {
595
1047
  options.signal?.throwIfAborted();
596
1048
  if (options.maxBytes !== undefined)
@@ -610,19 +1062,26 @@ export class MemoryFileSystem {
610
1062
  }
611
1063
  if (!node)
612
1064
  this.fail("EAGAIN", "fileStaging", path);
1065
+ if (!unchanged) {
1066
+ const currentScope = ownedStores.get(this)?.intact() ? this.identityScope : undefined;
1067
+ if (!expected.identityScope || expected.identityScope !== currentScope
1068
+ || !Number.isSafeInteger(expected.ino) || !Number.isSafeInteger(expected.dev))
1069
+ this.fail("ENOTSUP", "fileStaging", path);
1070
+ if (node.ino !== expected.ino || expected.dev !== 0 || node.type !== expected.type)
1071
+ this.fail("EAGAIN", "fileStaging", path);
1072
+ return;
1073
+ }
613
1074
  const current = this.snapshot(node);
614
1075
  if (!expected.identityScope || expected.identityScope !== current.identityScope
615
1076
  || !Number.isSafeInteger(expected.ino) || !Number.isSafeInteger(expected.dev))
616
1077
  this.fail("ENOTSUP", "fileStaging", path);
617
1078
  if (current.ino !== expected.ino || current.dev !== expected.dev || current.type !== expected.type)
618
1079
  this.fail("EAGAIN", "fileStaging", path);
619
- if (unchanged) {
620
- if (!Number.isSafeInteger(expected.revision) || !Number.isSafeInteger(current.revision))
621
- this.fail("ENOTSUP", "fileStaging", path);
622
- if (current.revision !== expected.revision || current.size !== expected.size || current.mode !== expected.mode
623
- || current.nlink !== expected.nlink || current.mtimeMs !== expected.mtimeMs || current.ctimeMs !== expected.ctimeMs)
624
- this.fail("EAGAIN", "fileStaging", path);
625
- }
1080
+ if (!Number.isSafeInteger(expected.revision) || !Number.isSafeInteger(current.revision))
1081
+ this.fail("ENOTSUP", "fileStaging", path);
1082
+ if (current.revision !== expected.revision || current.size !== expected.size || current.mode !== expected.mode
1083
+ || current.nlink !== expected.nlink || current.mtimeMs !== expected.mtimeMs || current.ctimeMs !== expected.ctimeMs)
1084
+ this.fail("EAGAIN", "fileStaging", path);
626
1085
  }
627
1086
  stagingLocations(staging) {
628
1087
  const parent = this.entry(staging.parent.path, "fileStaging");
@@ -639,7 +1098,10 @@ export class MemoryFileSystem {
639
1098
  return { directory, file };
640
1099
  }
641
1100
  async createStagedFile(directoryPath, name, content, options) {
1101
+ options = snapshotStagingCreation(options, directoryPath);
642
1102
  options.signal?.throwIfAborted();
1103
+ if (options.retainCleanup === true && this.capabilities.retainedStagingCleanup !== true)
1104
+ this.fail("ENOTSUP", "createStagedFile", directoryPath);
643
1105
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
644
1106
  this.fail("EINVAL", "createStagedFile", directoryPath);
645
1107
  const location = this.entry(directoryPath, "createStagedFile", true);
@@ -658,7 +1120,11 @@ export class MemoryFileSystem {
658
1120
  if (content.type === "symlink")
659
1121
  this.validatePath(content.target, "createStagedFile");
660
1122
  const retained = (location.name.length + name.length) * 2 + (content.type === "symlink" ? content.target.length * 2 : 0);
661
- this.ledger.reserve(retained, 4, "createStagedFile", directoryPath);
1123
+ const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
1124
+ const filePath = `${location.path}/${name}`;
1125
+ const cleanupBytes = options.retainCleanup ? (parentPath.length + location.path.length + filePath.length) * 2 : 0;
1126
+ const cleanupUnits = options.retainCleanup ? 3 : 0;
1127
+ this.ledger.reserve(retained + cleanupBytes, 4 + cleanupUnits, "createStagedFile", directoryPath);
662
1128
  let allocation;
663
1129
  let directory;
664
1130
  let file;
@@ -667,7 +1133,7 @@ export class MemoryFileSystem {
667
1133
  allocation = this.bytes(content.data, "createStagedFile", directoryPath);
668
1134
  directory = this.directory(0o700);
669
1135
  file = content.type === "file"
670
- ? { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation: allocation }
1136
+ ? (() => { const m = this.metadata(typeModes.file | mode); return new MemoryFileNode(m.mode, m.ino, m.atimeMs, allocation.data.byteLength, allocation, allocation.data); })()
671
1137
  : { ...this.metadata(typeModes.symlink | mode), type: "symlink", target: content.target };
672
1138
  if (options.atimeMs !== undefined)
673
1139
  file.atimeMs = options.atimeMs;
@@ -680,49 +1146,148 @@ export class MemoryFileSystem {
680
1146
  }
681
1147
  catch (error) {
682
1148
  allocation?.release();
683
- this.ledger.release(retained, 4);
1149
+ this.ledger.release(retained + cleanupBytes, 4 + cleanupUnits);
684
1150
  throw error;
685
1151
  }
686
1152
  this.changed(location.parent);
687
- const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
688
1153
  const receipt = (path, node) => Object.freeze({ path, stat: Object.freeze(this.snapshot(node)) });
689
- return Object.freeze({ parent: receipt(parentPath, location.parent), directory: receipt(location.path, directory), file: receipt(`${location.path}/${name}`, file) });
1154
+ const staging = Object.freeze({ parent: receipt(parentPath, location.parent), directory: receipt(location.path, directory), file: receipt(filePath, file) });
1155
+ if (!options.retainCleanup)
1156
+ return staging;
1157
+ // Reservations above include these references before either entry is exposed.
1158
+ location.parent.references++;
1159
+ directory.references++;
1160
+ file.references++;
1161
+ return Object.freeze({ ...staging, cleanup: this.retainStagingCleanup(staging, location.parent, directory, file, location.name, name) });
1162
+ }
1163
+ retainStagingCleanup(staging, parent, directory, file, directoryName, fileName) {
1164
+ // Clear this state on release, so keeping the closed handle does not keep
1165
+ // detached nodes alive outside the Memory ledger.
1166
+ let retained = { parent, directory, file };
1167
+ return createStagingCleanup(staging.directory.path, options => {
1168
+ options.signal?.throwIfAborted();
1169
+ const state = retained;
1170
+ if (state.parent.nlink === 0 || state.parent.entries.get(directoryName) !== state.directory)
1171
+ this.fail("EAGAIN", "removeStagedFile", staging.directory.path);
1172
+ this.expectEntry(state.parent, staging.parent.stat, staging.parent.path, false);
1173
+ this.expectEntry(state.directory, staging.directory.stat, staging.directory.path, false);
1174
+ if ((state.directory.mode & 0o777) !== 0o700)
1175
+ this.fail("EAGAIN", "removeStagedFile", staging.directory.path);
1176
+ const child = state.directory.entries.get(fileName);
1177
+ if (child && child !== state.file)
1178
+ this.fail("EAGAIN", "removeStagedFile", staging.file.path);
1179
+ this.removeStagingLocations(staging, {
1180
+ node: state.directory, parent: state.parent, name: directoryName, path: staging.directory.path,
1181
+ }, { node: child, parent: state.directory, name: fileName, path: staging.file.path });
1182
+ }, () => {
1183
+ const state = retained;
1184
+ retained = undefined;
1185
+ this.releaseReference(state.file, staging.file.path);
1186
+ this.releaseReference(state.directory, staging.directory.path);
1187
+ this.releaseReference(state.parent, staging.parent.path);
1188
+ });
690
1189
  }
691
- async publishStagedFile(staging, destination, options) {
692
- options.signal?.throwIfAborted();
693
- if (options.ancestors) {
694
- let path = "/";
695
- const parentPath = destination.slice(0, destination.lastIndexOf("/")) || "/";
696
- const paths = ["/", ...parentPath.split("/").filter(Boolean).map(component => {
697
- path = path === "/" ? `/${component}` : `${path}/${component}`;
698
- return path;
699
- })];
700
- if (paths.length !== options.ancestors.length)
701
- this.fail("EINVAL", "publishStagedFile", destination);
702
- for (let index = 0; index < paths.length; index++) {
703
- const expected = options.ancestors[index];
704
- if (expected.path !== paths[index])
705
- this.fail("EINVAL", "publishStagedFile", destination);
706
- const entry = this.entry(expected.path, "publishStagedFile");
707
- if (entry.node?.type !== "directory")
708
- this.fail("EAGAIN", "publishStagedFile", destination);
709
- this.expectEntry(entry.node, expected.stat, expected.path, false);
1190
+ async prepareDirectoryAncestry(ancestors, options = {}) {
1191
+ const entries = snapshotDirectoryAncestry(ancestors);
1192
+ const controls = options.signal === undefined ? {} : { signal: options.signal };
1193
+ controls.signal?.throwIfAborted();
1194
+ return () => this.verifyDirectoryAncestry(entries, controls);
1195
+ }
1196
+ async prepareStagingResolution(path, options = {}) {
1197
+ const controls = options.signal === undefined ? {} : { signal: options.signal };
1198
+ controls.signal?.throwIfAborted();
1199
+ this.validatePath(path, "prepareStagingResolution");
1200
+ if (!path.startsWith("/") || normalizePath(path) !== path)
1201
+ this.fail("EINVAL", "prepareStagingResolution", path);
1202
+ const traversed = [];
1203
+ const location = this.resolve(path, "prepareStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: traversed });
1204
+ if (location.node && location.node.type !== "file")
1205
+ this.fail("ENOTSUP", "prepareStagingResolution", path);
1206
+ const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
1207
+ const ancestors = snapshotDirectoryAncestry(directoryAncestryPaths(parentPath).map(entryPath => ({
1208
+ path: entryPath, stat: this.snapshot(this.entry(entryPath, "prepareStagingResolution").node),
1209
+ })));
1210
+ const steps = traversed.map(entry => ({ ...entry, stat: { ...entry.stat } }));
1211
+ const canonical = location.path;
1212
+ const destination = location.node ? this.snapshot(location.node) : null;
1213
+ const validate = () => {
1214
+ controls.signal?.throwIfAborted();
1215
+ const currentSteps = [];
1216
+ let current;
1217
+ try {
1218
+ current = this.resolve(path, "verifyStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: currentSteps });
1219
+ }
1220
+ catch (error) {
1221
+ if (error instanceof FsError && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code))
1222
+ this.fail("EAGAIN", "verifyStagingResolution", path);
1223
+ throw error;
1224
+ }
1225
+ if (current.path !== canonical || steps.length !== currentSteps.length)
1226
+ this.fail("EAGAIN", "verifyStagingResolution", path);
1227
+ for (const [index, before] of steps.entries()) {
1228
+ const after = currentSteps[index];
1229
+ if (before.path !== after.path || before.linkTarget !== after.linkTarget || before.stat.type !== after.stat.type
1230
+ || compareIdentity(before.stat, after.stat) !== "same"
1231
+ || before.stat.type !== "directory" && ["revision", "size", "mode", "nlink", "mtimeMs", "ctimeMs"].some(field => before.stat[field] !== after.stat[field])) {
1232
+ this.fail("EAGAIN", "verifyStagingResolution", before.path);
1233
+ }
710
1234
  }
1235
+ this.expectEntry(current.node, destination, path);
1236
+ return this.verifyDirectoryAncestry(ancestors, controls);
1237
+ };
1238
+ return snapshotStagingResolution({ path: canonical, parent: this.snapshot(location.parent), destination, ancestors, traversed: steps, validate });
1239
+ }
1240
+ verifyDirectoryAncestry(ancestors, options) {
1241
+ const entries = snapshotDirectoryAncestry(ancestors);
1242
+ options.signal?.throwIfAborted();
1243
+ let node = this.root;
1244
+ for (const [index, expected] of entries.entries()) {
1245
+ if (index > 0)
1246
+ node = node.entries.get(expected.path.slice(expected.path.lastIndexOf("/") + 1));
1247
+ if (node?.type !== "directory")
1248
+ this.fail("EAGAIN", "verifyDirectoryAncestry", expected.path);
1249
+ this.expectEntry(node, expected.stat, expected.path, false);
1250
+ this.permission(node, 1, "verifyDirectoryAncestry", expected.path);
711
1251
  }
1252
+ return true;
1253
+ }
1254
+ async publishStagedFile(receipt, destination, options) {
1255
+ const signal = options.signal;
1256
+ const guard = options.commitGuard;
1257
+ const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
1258
+ const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
1259
+ const staging = { parent: snapshot(receipt.parent), directory: snapshot(receipt.directory), file: snapshot(receipt.file) };
1260
+ const parent = { ...options.parent };
1261
+ const expected = options.destination === null ? null : { ...options.destination };
1262
+ const parentPath = destination.slice(0, destination.lastIndexOf("/")) || "/";
1263
+ if (ancestors && ancestors.at(-1)?.path !== parentPath)
1264
+ this.fail("EINVAL", "publishStagedFile", destination);
1265
+ const controls = signal === undefined ? {} : { signal };
1266
+ signal?.throwIfAborted();
1267
+ if (guard !== undefined)
1268
+ runStagingGuard(guard);
1269
+ signal?.throwIfAborted();
1270
+ // All inputs were copied before the guard. Neither outer validation nor
1271
+ // these local checks yield before the stock rename commits.
1272
+ if (ancestors)
1273
+ runStagingGuard(() => this.verifyDirectoryAncestry(ancestors, controls));
712
1274
  const { directory, file } = this.stagingLocations(staging);
713
1275
  this.expectEntry(file.node, staging.file.stat, staging.file.path);
714
1276
  const target = this.entry(destination, "publishStagedFile", true);
715
- this.expectEntry(target.parent, options.parent, destination, false);
716
- this.expectEntry(target.node, options.destination, destination);
1277
+ this.expectEntry(target.parent, parent, destination, false);
1278
+ this.expectEntry(target.node, expected, destination);
717
1279
  if (target.parent === directory.node || target.node === directory.node || target.node === file.node)
718
1280
  this.fail("EINVAL", "publishStagedFile", destination);
719
- if (target.node && (target.node.type !== "file" || target.node.nlink !== 1))
1281
+ if (target.node && target.node.type !== "file")
720
1282
  this.fail("EAGAIN", "publishStagedFile", destination);
721
- return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...options, noReplace: options.destination === null });
1283
+ return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
722
1284
  }
723
1285
  async removeStagedFile(staging, options = {}) {
724
1286
  options.signal?.throwIfAborted();
725
1287
  const { directory, file } = this.stagingLocations(staging);
1288
+ this.removeStagingLocations(staging, directory, file);
1289
+ }
1290
+ removeStagingLocations(staging, directory, file) {
726
1291
  if (file.node)
727
1292
  this.expectEntry(file.node, staging.file.stat, staging.file.path);
728
1293
  const node = directory.node;
@@ -762,8 +1327,10 @@ export class MemoryFileSystem {
762
1327
  }
763
1328
  if (node.type !== "directory")
764
1329
  this.fail("ENOTDIR", "prepareDirectory", path);
765
- if (mode !== undefined)
1330
+ if (mode !== undefined) {
766
1331
  node.mode = typeModes.directory | mode;
1332
+ memoryCaches.get(this.ledger).clearWrites();
1333
+ }
767
1334
  if (options.atimeMs !== undefined)
768
1335
  node.atimeMs = options.atimeMs;
769
1336
  if (options.mtimeMs !== undefined)
@@ -779,11 +1346,20 @@ export class MemoryFileSystem {
779
1346
  this.expectEntry(location.node, options.expected, path);
780
1347
  if (location.node && location.node.type !== "file")
781
1348
  this.fail("EINVAL", "writeFileConditional", path);
782
- this.writeData(path, data, { ...(options.mode === undefined ? {} : { mode: options.mode }), flag: options.append ? "a" : "w" }, "writeFileConditional");
783
- return Object.freeze(this.snapshot(location.parent.entries.get(location.name)));
1349
+ if (options.atimeMs !== undefined)
1350
+ this.integer(options.atimeMs, "writeFileConditional", path);
1351
+ if (options.mtimeMs !== undefined)
1352
+ this.integer(options.mtimeMs, "writeFileConditional", path);
1353
+ const node = this.writeData(path, data, { ...(options.mode === undefined ? {} : { mode: options.mode }), flag: options.append ? "a" : "w" }, "writeFileConditional");
1354
+ if (options.atimeMs !== undefined)
1355
+ node.atimeMs = options.atimeMs;
1356
+ if (options.mtimeMs !== undefined)
1357
+ node.mtimeMs = options.mtimeMs;
1358
+ return Object.freeze(this.snapshot(node));
784
1359
  }
785
1360
  async removeEntryConditional(path, options) {
786
1361
  options.signal?.throwIfAborted();
1362
+ const returnRemainingStat = options.returnRemainingStat === true;
787
1363
  const location = this.entry(path, "removeEntryConditional", true);
788
1364
  this.expectEntry(location.parent, options.parent, path, false);
789
1365
  this.expectEntry(location.node, options.expected, path);
@@ -798,12 +1374,15 @@ export class MemoryFileSystem {
798
1374
  if (node.type === "directory" && node.entries.size > 0)
799
1375
  this.fail("ENOTEMPTY", "removeEntryConditional", path);
800
1376
  location.parent.entries.delete(location.name);
1377
+ memoryCaches.get(this.ledger).clearWrites();
801
1378
  this.ledger.release(location.name.length * 2, 1);
802
1379
  node.nlink--;
803
1380
  node.ctimeMs = Date.now();
804
1381
  node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
805
1382
  this.releaseNode(node);
806
1383
  this.changed(location.parent);
1384
+ if (returnRemainingStat)
1385
+ return Object.freeze(this.snapshot(node));
807
1386
  }
808
1387
  async removeFileConditional(path, options) {
809
1388
  options.signal?.throwIfAborted();
@@ -815,6 +1394,7 @@ export class MemoryFileSystem {
815
1394
  this.fail("EINVAL", "removeFileConditional", path);
816
1395
  this.permission(location.parent, 3, "removeFileConditional", path);
817
1396
  location.parent.entries.delete(location.name);
1397
+ memoryCaches.get(this.ledger).clearWrites();
818
1398
  this.ledger.release(location.name.length * 2, 1);
819
1399
  node.nlink--;
820
1400
  node.ctimeMs = Date.now();
@@ -841,40 +1421,50 @@ export class MemoryFileSystem {
841
1421
  }
842
1422
  async stat(path, options = {}) {
843
1423
  options.signal?.throwIfAborted();
844
- return this.snapshot(this.resolve(path, "stat").node);
1424
+ return this.snapshot(this.resolveNode(path, "stat", true));
845
1425
  }
846
1426
  async lstat(path, options = {}) {
847
1427
  options.signal?.throwIfAborted();
848
- const stat = this.snapshot(this.resolve(path, "lstat", { followFinal: false }).node);
1428
+ const stat = this.snapshot(this.resolveNode(path, "lstat", false));
849
1429
  ownedStats.set(stat, { filesystem: this, path, root: this.root });
850
1430
  return stat;
851
1431
  }
852
1432
  async readdir(path, options = {}) {
853
1433
  const limit = directoryEntryLimit(options, path);
854
- const node = this.resolve(path, "readdir").node;
1434
+ const node = this.resolveNode(path, "readdir", true);
855
1435
  if (node.type !== "directory")
856
1436
  this.fail("ENOTDIR", "readdir", path);
857
1437
  this.permission(node, 4, "readdir", path);
858
1438
  admitDirectoryEntries(node.entries.size, limit, path);
859
1439
  node.atimeMs = Date.now();
860
- return [...node.entries].map(([name, entry]) => ({ name, type: entry.type }))
861
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1440
+ const result = new Array(node.entries.size);
1441
+ let idx = 0;
1442
+ for (const [name, entry] of node.entries) {
1443
+ result[idx++] = { name, type: entry.type };
1444
+ }
1445
+ return result.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
862
1446
  }
863
- async mkdir(path, options = {}) {
864
- options.signal?.throwIfAborted();
865
- this.validatePath(path, "mkdir");
866
- const mode = this.mode(options.mode, 0o777, "mkdir", path);
867
- if (options.recursive) {
868
- const node = this.resolve(path, "mkdir", { createDirectories: mode }).node;
869
- if (node.type !== "directory")
1447
+ mkdir(path, options = {}) {
1448
+ try {
1449
+ options.signal?.throwIfAborted();
1450
+ this.validatePath(path, "mkdir");
1451
+ const mode = this.mode(options.mode, 0o777, "mkdir", path);
1452
+ if (options.recursive) {
1453
+ const node = this.resolve(path, "mkdir", { createDirectories: mode }).node;
1454
+ if (node.type !== "directory")
1455
+ this.fail("EEXIST", "mkdir", path);
1456
+ return resolvedVoid;
1457
+ }
1458
+ const location = this.resolve(path.replace(/\/+$/, "") || "/", "mkdir", { allowMissing: true, followFinal: false });
1459
+ if (location.node)
870
1460
  this.fail("EEXIST", "mkdir", path);
871
- return;
1461
+ this.permission(location.parent, 3, "mkdir", path);
1462
+ this.addNode(location.parent, location.name, () => this.directory(mode), "mkdir", path);
1463
+ return resolvedVoid;
1464
+ }
1465
+ catch (error) {
1466
+ return Promise.reject(error);
872
1467
  }
873
- const location = this.resolve(path.replace(/\/+$/, "") || "/", "mkdir", { allowMissing: true, followFinal: false });
874
- if (location.node)
875
- this.fail("EEXIST", "mkdir", path);
876
- this.permission(location.parent, 3, "mkdir", path);
877
- this.addNode(location.parent, location.name, () => this.directory(mode), "mkdir", path);
878
1468
  }
879
1469
  async rmdir(path, options = {}) {
880
1470
  options.signal?.throwIfAborted();
@@ -897,18 +1487,38 @@ export class MemoryFileSystem {
897
1487
  this.releaseNode(node);
898
1488
  this.changed(location.parent);
899
1489
  }
900
- async rm(path, options = {}) {
901
- options.signal?.throwIfAborted();
902
- let location;
1490
+ rm(path, options = {}) {
903
1491
  try {
904
- location = this.entry(path, "rm");
1492
+ options.signal?.throwIfAborted();
1493
+ const bound = activeConditionalMutations.get(this.identityScope);
1494
+ const active = bound?.path === path ? bound : undefined;
1495
+ const ancestors = options.ancestors ?? active?.ancestors;
1496
+ const expectedParent = options.parent ?? active?.parent;
1497
+ const expectedNode = options.expected ?? active?.expected;
1498
+ if (ancestors !== undefined)
1499
+ this.verifyDirectoryAncestry(ancestors, options);
1500
+ let location;
1501
+ try {
1502
+ location = this.entry(path, "rm", expectedNode !== undefined || expectedParent !== undefined);
1503
+ }
1504
+ catch (error) {
1505
+ if (options.force && error instanceof FsError && error.code === "ENOENT")
1506
+ return resolvedVoid;
1507
+ throw error;
1508
+ }
1509
+ if (expectedParent !== undefined)
1510
+ this.expectEntry(location.parent, expectedParent, path, false);
1511
+ if (expectedNode !== undefined) {
1512
+ if (options.force && !location.node)
1513
+ return resolvedVoid;
1514
+ this.expectEntry(location.node, expectedNode, path, location.node?.type !== "directory");
1515
+ }
1516
+ this.removeLocation(location, path, "rm", options.recursive === true);
1517
+ return resolvedVoid;
905
1518
  }
906
1519
  catch (error) {
907
- if (options.force && error instanceof FsError && error.code === "ENOENT")
908
- return;
909
- throw error;
1520
+ return Promise.reject(error);
910
1521
  }
911
- this.removeLocation(location, path, "rm", options.recursive === true);
912
1522
  }
913
1523
  removeLocation(location, path, syscall, recursive) {
914
1524
  const node = location.node;
@@ -919,21 +1529,25 @@ export class MemoryFileSystem {
919
1529
  this.permission(location.parent, 3, syscall, path);
920
1530
  if (node.type === "directory" && !recursive)
921
1531
  this.fail("EISDIR", syscall, path);
1532
+ memoryCaches.get(this.ledger).clearWrites();
922
1533
  const removed = [node];
1534
+ let keyChars = 0;
1535
+ let keyEntries = 0;
923
1536
  for (let index = 0; index < removed.length; index++) {
924
1537
  const entry = removed[index];
925
1538
  if (entry.type === "directory" && entry.entries.size > 0) {
926
1539
  this.permission(entry, 7, syscall, path);
927
- for (const child of entry.entries.values())
1540
+ entry.entries.forEach((child, name) => {
928
1541
  removed.push(child);
1542
+ keyChars += name.length;
1543
+ keyEntries++;
1544
+ });
929
1545
  }
930
1546
  }
931
1547
  location.parent.entries.delete(location.name);
932
- this.ledger.release(location.name.length * 2, 1);
1548
+ this.ledger.release((location.name.length + keyChars) * 2, 1 + keyEntries);
933
1549
  for (const entry of removed) {
934
1550
  if (entry.type === "directory") {
935
- for (const name of entry.entries.keys())
936
- this.ledger.release(name.length * 2, 1);
937
1551
  entry.entries.clear();
938
1552
  }
939
1553
  entry.nlink--;
@@ -984,6 +1598,7 @@ export class MemoryFileSystem {
984
1598
  this.ledger.release(Math.max(0, nameGrowth), 0);
985
1599
  throw error;
986
1600
  }
1601
+ memoryCaches.get(this.ledger).clearWrites();
987
1602
  origin.parent.entries.delete(origin.name);
988
1603
  if (target.node) {
989
1604
  this.ledger.release(origin.name.length * 2, 1);
@@ -1029,11 +1644,17 @@ export class MemoryFileSystem {
1029
1644
  options.signal?.throwIfAborted();
1030
1645
  return this.resolve(path, "realpath").path;
1031
1646
  }
1032
- async access(path, mode = 0, options = {}) {
1033
- options.signal?.throwIfAborted();
1034
- if (!Number.isInteger(mode) || mode < 0 || mode > 7)
1035
- this.fail("EINVAL", "access", path);
1036
- this.permission(this.resolve(path, "access").node, mode, "access", path);
1647
+ access(path, mode = 0, options = {}) {
1648
+ try {
1649
+ options.signal?.throwIfAborted();
1650
+ if (!Number.isInteger(mode) || mode < 0 || mode > 7)
1651
+ this.fail("EINVAL", "access", path);
1652
+ this.permission(this.resolveNode(path, "access", true), mode, "access", path);
1653
+ return resolvedVoid;
1654
+ }
1655
+ catch (error) {
1656
+ return Promise.reject(error);
1657
+ }
1037
1658
  }
1038
1659
  async readlink(path, options = {}) {
1039
1660
  options.signal?.throwIfAborted();
@@ -1078,7 +1699,28 @@ export class MemoryFileSystem {
1078
1699
  async chmod(path, mode, options = {}) {
1079
1700
  options.signal?.throwIfAborted();
1080
1701
  const permissions = this.mode(mode, 0, "chmod", path);
1081
- const node = this.resolve(path, "chmod").node;
1702
+ const bound = activeConditionalMutations.get(this.identityScope);
1703
+ const active = bound?.path === path ? bound : undefined;
1704
+ const conditional = snapshotConditionalChmod(path, active ? { ...active, ...options } : options);
1705
+ const controls = conditional ?? options;
1706
+ const ancestors = conditional?.ancestors;
1707
+ const expectedParent = conditional?.parent;
1708
+ const expectedNode = conditional?.expected;
1709
+ if (conditional?.commitGuard)
1710
+ runStagingGuard(conditional.commitGuard);
1711
+ controls.signal?.throwIfAborted();
1712
+ if (ancestors !== undefined)
1713
+ this.verifyDirectoryAncestry(ancestors, controls);
1714
+ const location = expectedNode !== undefined || expectedParent !== undefined
1715
+ ? this.entry(path, "chmod", true)
1716
+ : this.resolve(path, "chmod");
1717
+ if (expectedParent !== undefined)
1718
+ this.expectEntry(location.parent, expectedParent, path, false);
1719
+ if (expectedNode !== undefined)
1720
+ this.expectEntry(location.node, expectedNode, path, location.node?.type !== "directory");
1721
+ const node = location.node;
1722
+ if (node.type === "directory")
1723
+ memoryCaches.get(this.ledger).clearWrites();
1082
1724
  node.mode = typeModes[node.type] | permissions;
1083
1725
  node.ctimeMs = Date.now();
1084
1726
  node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
@@ -1239,39 +1881,95 @@ export class MemoryFileSystem {
1239
1881
  },
1240
1882
  };
1241
1883
  }
1242
- async *readStream(path, options = {}) {
1243
- options.signal?.throwIfAborted();
1244
- const start = options.start ?? 0;
1245
- const chunkSize = options.chunkSize ?? preferredIoBlockSize;
1246
- this.integer(start, "readStream", path);
1247
- this.integer(chunkSize, "readStream", path);
1248
- if (chunkSize === 0)
1249
- this.fail("EINVAL", "readStream", path);
1250
- if (options.endExclusive !== undefined) {
1251
- this.integer(options.endExclusive, "readStream", path);
1252
- if (options.endExclusive < start)
1253
- this.fail("EINVAL", "readStream", path);
1254
- }
1255
- const node = this.file(path, "readStream");
1256
- this.permission(node, 4, "readStream", path);
1257
- this.ledger.reserve(path.length * 2, 1, "readStream", path);
1258
- node.references++;
1259
- const allocation = node.allocation;
1260
- allocation.retain();
1261
- const data = node.data;
1262
- try {
1263
- const end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
1264
- node.atimeMs = Date.now();
1265
- for (let offset = start; offset < end; offset += chunkSize) {
1266
- options.signal?.throwIfAborted();
1267
- yield data.slice(offset, Math.min(offset + chunkSize, end));
1268
- }
1269
- options.signal?.throwIfAborted();
1270
- }
1271
- finally {
1272
- allocation.release();
1273
- this.releaseReference(node, path);
1274
- }
1884
+ readStream(path, options = {}) {
1885
+ return {
1886
+ [Symbol.asyncIterator]: () => {
1887
+ let initialized = false;
1888
+ let finished = false;
1889
+ let offset = 0;
1890
+ let end = 0;
1891
+ let chunkSize = preferredIoBlockSize;
1892
+ let node;
1893
+ let allocation;
1894
+ let data;
1895
+ const release = () => {
1896
+ if (finished)
1897
+ return;
1898
+ finished = true;
1899
+ if (allocation) {
1900
+ allocation.release();
1901
+ allocation = undefined;
1902
+ }
1903
+ if (node) {
1904
+ const releasedNode = node;
1905
+ node = undefined;
1906
+ this.releaseReference(releasedNode, path);
1907
+ }
1908
+ data = undefined;
1909
+ };
1910
+ const step = () => {
1911
+ if (finished)
1912
+ return { done: true, value: undefined };
1913
+ try {
1914
+ options.signal?.throwIfAborted();
1915
+ if (!initialized) {
1916
+ initialized = true;
1917
+ const start = options.start ?? 0;
1918
+ chunkSize = options.chunkSize ?? preferredIoBlockSize;
1919
+ this.integer(start, "readStream", path);
1920
+ this.integer(chunkSize, "readStream", path);
1921
+ if (chunkSize === 0)
1922
+ this.fail("EINVAL", "readStream", path);
1923
+ if (options.endExclusive !== undefined) {
1924
+ this.integer(options.endExclusive, "readStream", path);
1925
+ if (options.endExclusive < start)
1926
+ this.fail("EINVAL", "readStream", path);
1927
+ }
1928
+ const target = this.file(path, "readStream");
1929
+ this.permission(target, 4, "readStream", path);
1930
+ this.ledger.reserve(path.length * 2, 1, "readStream", path);
1931
+ target.references++;
1932
+ node = target;
1933
+ allocation = target.allocation;
1934
+ allocation.retain();
1935
+ data = target.data;
1936
+ offset = start;
1937
+ end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
1938
+ target.atimeMs = Date.now();
1939
+ }
1940
+ if (offset < end) {
1941
+ const nextEnd = Math.min(offset + chunkSize, end);
1942
+ const chunk = data.slice(offset, nextEnd);
1943
+ offset = nextEnd;
1944
+ return { done: false, value: chunk };
1945
+ }
1946
+ release();
1947
+ return { done: true, value: undefined };
1948
+ }
1949
+ catch (error) {
1950
+ release();
1951
+ throw error;
1952
+ }
1953
+ };
1954
+ return {
1955
+ abortSignal: options.signal,
1956
+ [Symbol.asyncIterator]() { return this; },
1957
+ tryNextSync: step,
1958
+ next() {
1959
+ try {
1960
+ return Promise.resolve(step());
1961
+ }
1962
+ catch (error) {
1963
+ return Promise.reject(error);
1964
+ }
1965
+ },
1966
+ return() {
1967
+ release();
1968
+ return Promise.resolve({ done: true, value: undefined });
1969
+ },
1970
+ };
1971
+ },
1972
+ };
1275
1973
  }
1276
1974
  async writeStream(path, source, options = {}) {
1277
1975
  options.signal?.throwIfAborted();
@@ -1280,7 +1978,9 @@ export class MemoryFileSystem {
1280
1978
  let node;
1281
1979
  try {
1282
1980
  node = this.openWrite(path, options, "writeStream");
1981
+ const guard = options[extractionStreamGuard];
1283
1982
  node.references++;
1983
+ guard?.(node);
1284
1984
  const append = options.flag === "a" || options.flag === "ax";
1285
1985
  let position = 0;
1286
1986
  if (!append) {
@@ -1289,6 +1989,7 @@ export class MemoryFileSystem {
1289
1989
  }
1290
1990
  for await (const chunk of source) {
1291
1991
  options.signal?.throwIfAborted();
1992
+ guard?.(node);
1292
1993
  if (!(chunk instanceof Uint8Array))
1293
1994
  throw new TypeError("Memory files require Uint8Array data");
1294
1995
  this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
@@ -1306,37 +2007,251 @@ export class MemoryFileSystem {
1306
2007
  }
1307
2008
  }
1308
2009
  const memoryImplementation = Object.getOwnPropertyDescriptors(MemoryFileSystem.prototype);
2010
+ const memoryImplementationKeys = Object.keys(memoryImplementation);
2011
+ const memoryImplementationDescriptors = memoryImplementationKeys.map(key => memoryImplementation[key]);
2012
+ const MEMORY_DESCRIPTOR_CAPABILITIES = Object.freeze({
2013
+ noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
2014
+ });
2015
+ const memoryDescriptorGetPosition = async (retained) => retained.position;
2016
+ const memoryDescriptorStat = async (retained) => retained.fs.snapshot(retained.node);
2017
+ const memoryDescriptorRead = async (retained, buffer, position) => {
2018
+ const inode = retained.node;
2019
+ const start = position ?? retained.position;
2020
+ const count = Math.min(buffer.byteLength, Math.max(0, inode.data.byteLength - start));
2021
+ buffer.set(inode.data.subarray(start, start + count));
2022
+ if (position === null)
2023
+ retained.position += count;
2024
+ inode.atimeMs = Date.now();
2025
+ return count;
2026
+ };
2027
+ const memoryDescriptorWrite = async (retained, buffer, position) => {
2028
+ const inode = retained.node;
2029
+ const start = retained.append ? inode.data.byteLength : position ?? retained.position;
2030
+ const end = start + buffer.byteLength;
2031
+ retained.fs.writeAt(inode, buffer, start, "write", retained.path);
2032
+ if (position === null)
2033
+ retained.position = end;
2034
+ return buffer.byteLength;
2035
+ };
2036
+ const memoryDescriptorTruncate = async (retained, length) => {
2037
+ retained.fs.resizeNode(retained.node, length, "ftruncate", retained.path);
2038
+ };
2039
+ const memoryDescriptorSync = async () => { };
2040
+ const memoryDescriptorClose = async (retained) => {
2041
+ const inode = retained.node;
2042
+ retained.node = undefined;
2043
+ retained.fs.releaseReference(inode, retained.path);
2044
+ };
2045
+ const stockDescriptorWriteMethodNames = [
2046
+ "writeStream", "writeFile", "appendFile", "access", "stat", "lstat", "realpath",
2047
+ "openWrite", "prepareWrite", "writeData", "addNode", "replaceData", "releaseReference", "releaseNode",
2048
+ "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "admitSize", "changed", "metadata", "integer", "writeAt", "fail",
2049
+ ];
2050
+ const stockRetainedResizeMethodNames = [
2051
+ "openResizeFile", "truncate", "resizeNode", "snapshot",
2052
+ ];
1309
2053
  function stockDescriptorWrite(filesystem) {
1310
- if (Object.getPrototypeOf(filesystem) !== MemoryFileSystem.prototype)
2054
+ if (!isStockMemoryMethods(filesystem, stockDescriptorWriteMethodNames, true))
1311
2055
  return false;
1312
2056
  const owner = ownedStores.get(filesystem);
1313
- if (!owner || Object.getOwnPropertyDescriptor(filesystem, "root")?.value !== owner.root
1314
- || Object.getOwnPropertyDescriptor(filesystem, "capabilities")?.value !== owner.capabilities)
2057
+ const capDesc = Object.getOwnPropertyDescriptor(filesystem, "capabilities");
2058
+ return !!capDesc && "value" in capDesc && capDesc.value === owner.capabilities;
2059
+ }
2060
+ function stockRetainedResize(filesystem) {
2061
+ if (!stockDescriptorWrite(filesystem) || !isStockMemoryMethods(filesystem, stockRetainedResizeMethodNames, false))
1315
2062
  return false;
1316
- for (const name of ["writeStream", "writeFile", "appendFile", "access", "stat", "lstat", "realpath",
1317
- "openWrite", "prepareWrite", "writeData", "addNode", "replaceData", "releaseReference", "releaseNode",
1318
- "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "admitSize", "changed", "metadata", "integer", "writeAt", "fail"]) {
1319
- const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
1320
- ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
1321
- if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
2063
+ const owner = ownedStores.get(filesystem);
2064
+ const ledgerDesc = Object.getOwnPropertyDescriptor(filesystem, "ledger");
2065
+ return !!ledgerDesc && "value" in ledgerDesc && ledgerDesc.value === owner.ledger;
2066
+ }
2067
+ export function createMemoryFileSystem(options = {}) {
2068
+ return new MemoryFileSystem(options);
2069
+ }
2070
+ const missingTargetMethodNames = ["realpath", "lstat", "resolve", "permission", "validatePath", "fail", "snapshot"];
2071
+ const deviceFastMethodNames = ["lstat", "readlink", "resolve", "permission", "validatePath", "fail", "snapshot"];
2072
+ const readFileFastMethodNames = ["readFile", "file", "resolve", "permission", "validatePath", "fail", "integer"];
2073
+ const writeFileFastMethodNames = [
2074
+ "writeFile", "appendFile", "writeData", "prepareWrite", "openWrite", "writeAt",
2075
+ "addNode", "replaceData", "resolve", "permission", "validatePath", "mode",
2076
+ "bytes", "allocate", "admitSize", "changed", "metadata", "fail",
2077
+ ];
2078
+ export function utf8ByteLength(value) {
2079
+ let bytes = 0;
2080
+ for (let i = 0; i < value.length; i++) {
2081
+ const code = value.charCodeAt(i);
2082
+ if (code < 0x80)
2083
+ bytes += 1;
2084
+ else if (code < 0x800)
2085
+ bytes += 2;
2086
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
2087
+ bytes += 4;
2088
+ i++;
2089
+ }
2090
+ else
2091
+ bytes += 3;
2092
+ }
2093
+ return bytes;
2094
+ }
2095
+ function exceedsComponentByteLimit(value) {
2096
+ return value.length > 85 && (value.length > 255 || utf8ByteLength(value) > 255);
2097
+ }
2098
+ export function isCleanAbsolutePath(path) {
2099
+ const len = path.length;
2100
+ if (len <= 1 || len > 65536 || path.charCodeAt(0) !== 47 || path.charCodeAt(len - 1) === 47)
2101
+ return false;
2102
+ let segStart = 1;
2103
+ let components = 1;
2104
+ for (let i = 1; i < len; i++) {
2105
+ const c = path.charCodeAt(i);
2106
+ if (c === 0)
1322
2107
  return false;
2108
+ if (c === 47) {
2109
+ const segLen = i - segStart;
2110
+ if (segLen === 0)
2111
+ return false;
2112
+ if (segLen === 1 && path.charCodeAt(segStart) === 46)
2113
+ return false;
2114
+ if (segLen === 2 && path.charCodeAt(segStart) === 46 && path.charCodeAt(segStart + 1) === 46)
2115
+ return false;
2116
+ if (++components > 256)
2117
+ return false;
2118
+ segStart = i + 1;
2119
+ }
1323
2120
  }
2121
+ const lastLen = len - segStart;
2122
+ if (lastLen === 1 && path.charCodeAt(segStart) === 46)
2123
+ return false;
2124
+ if (lastLen === 2 && path.charCodeAt(segStart) === 46 && path.charCodeAt(segStart + 1) === 46)
2125
+ return false;
1324
2126
  return true;
1325
2127
  }
1326
- function stockRetainedResize(filesystem) {
1327
- if (!stockDescriptorWrite(filesystem))
2128
+ export function tryResolveMemoryDevicePath(filesystem, path, resizeCreate) {
2129
+ const mem = filesystem;
2130
+ if (!ownedStores.has(mem) || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, deviceFastMethodNames, false))
2131
+ return undefined;
2132
+ if (!isCleanAbsolutePath(path) || path === "/dev" || path.startsWith("/dev/"))
2133
+ return undefined;
2134
+ let current = mem.root;
2135
+ let start = 1;
2136
+ while (true) {
2137
+ if (((current.mode >> 6) & 1) !== 1)
2138
+ return undefined;
2139
+ const slash = path.indexOf("/", start);
2140
+ if (slash === -1) {
2141
+ const name = path.slice(start);
2142
+ if (exceedsComponentByteLimit(name))
2143
+ return undefined;
2144
+ if (resizeCreate === false && !current.entries.has(name))
2145
+ return undefined;
2146
+ return path;
2147
+ }
2148
+ const name = path.slice(start, slash);
2149
+ if (exceedsComponentByteLimit(name))
2150
+ return undefined;
2151
+ const next = current.entries.get(name);
2152
+ if (!next) {
2153
+ if (resizeCreate !== undefined)
2154
+ return undefined;
2155
+ let remStart = slash + 1;
2156
+ while (true) {
2157
+ const nextSlash = path.indexOf("/", remStart);
2158
+ const seg = nextSlash === -1 ? path.slice(remStart) : path.slice(remStart, nextSlash);
2159
+ if (exceedsComponentByteLimit(seg))
2160
+ return undefined;
2161
+ if (nextSlash === -1)
2162
+ return path;
2163
+ remStart = nextSlash + 1;
2164
+ }
2165
+ }
2166
+ if (next.type !== "directory")
2167
+ return undefined;
2168
+ current = next;
2169
+ start = slash + 1;
2170
+ }
2171
+ }
2172
+ function isStockMemoryMethods(mem, names, checkRootAccessor = true) {
2173
+ const owner = ownedStores.get(mem);
2174
+ if (!owner || Object.getPrototypeOf(mem) !== MemoryFileSystem.prototype)
1328
2175
  return false;
1329
- const owner = ownedStores.get(filesystem);
1330
- if (!owner || Object.getOwnPropertyDescriptor(filesystem, "ledger")?.value !== owner.ledger)
2176
+ if (checkRootAccessor) {
2177
+ const rootDesc = Object.getOwnPropertyDescriptor(mem, "root");
2178
+ if (!rootDesc || !("value" in rootDesc) || rootDesc.value !== owner.root)
2179
+ return false;
2180
+ }
2181
+ else if (mem.root !== owner.root) {
1331
2182
  return false;
1332
- for (const name of ["openResizeFile", "truncate", "resizeNode", "snapshot"]) {
1333
- const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
1334
- ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
1335
- if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
2183
+ }
2184
+ for (let i = 0; i < names.length; i++) {
2185
+ const name = names[i];
2186
+ if (Object.prototype.hasOwnProperty.call(mem, name)) {
2187
+ const descriptor = Object.getOwnPropertyDescriptor(mem, name);
2188
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
2189
+ return false;
2190
+ }
2191
+ else if (MemoryFileSystem.prototype[name] !== memoryImplementation[name]?.value) {
1336
2192
  return false;
2193
+ }
1337
2194
  }
1338
2195
  return true;
1339
2196
  }
1340
- export function createMemoryFileSystem(options = {}) {
1341
- return new MemoryFileSystem(options);
2197
+ export function tryReadMemoryFileViewSync(filesystem, path, maxBytes, signal) {
2198
+ const mem = filesystem;
2199
+ if (!ownedStores.has(mem) || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
2200
+ return undefined;
2201
+ signal?.throwIfAborted();
2202
+ if (maxBytes !== undefined)
2203
+ mem.integer(maxBytes, "readFile", path);
2204
+ const node = mem.file(path, "readFile");
2205
+ mem.permission(node, 4, "readFile", path);
2206
+ if (maxBytes !== undefined && node.data.byteLength > maxBytes)
2207
+ mem.fail("EFBIG", "readFile", path);
2208
+ node.atimeMs = Date.now();
2209
+ return node.data;
2210
+ }
2211
+ export function tryGetMemoryDirectoryEntryNamesSync(filesystem, path) {
2212
+ const mem = filesystem;
2213
+ if (!ownedStores.has(mem) || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
2214
+ return undefined;
2215
+ const root = mem.root;
2216
+ if (path === "/") {
2217
+ if (((root.mode >> 6) & 4) !== 4)
2218
+ return undefined;
2219
+ return root.entries;
2220
+ }
2221
+ if (!isCleanAbsolutePath(path) || path === "/dev" || path.startsWith("/dev/"))
2222
+ return undefined;
2223
+ let current = root;
2224
+ let start = 1;
2225
+ while (true) {
2226
+ if (((current.mode >> 6) & 1) !== 1)
2227
+ return undefined;
2228
+ const slash = path.indexOf("/", start);
2229
+ if (slash === -1) {
2230
+ const next = current.entries.get(path.slice(start));
2231
+ if (!next || next.type !== "directory" || ((next.mode >> 6) & 4) !== 4)
2232
+ return undefined;
2233
+ return next.entries;
2234
+ }
2235
+ const next = current.entries.get(path.slice(start, slash));
2236
+ if (!next || next.type !== "directory")
2237
+ return undefined;
2238
+ current = next;
2239
+ start = slash + 1;
2240
+ }
2241
+ }
2242
+ export function tryWriteMemoryFileSync(filesystem, path, data, append, mode, signal) {
2243
+ const mem = filesystem;
2244
+ const owner = ownedStores.get(mem);
2245
+ if (!owner ||
2246
+ mem.symlinkCount !== 0 ||
2247
+ mem.capabilities !== owner.capabilities ||
2248
+ !isStockMemoryMethods(mem, writeFileFastMethodNames, false) ||
2249
+ !isCleanAbsolutePath(path) ||
2250
+ path === "/dev" ||
2251
+ path.startsWith("/dev/")) {
2252
+ return false;
2253
+ }
2254
+ signal?.throwIfAborted();
2255
+ mem.writeMemoryFileFast(path, data, append, mode);
2256
+ return true;
1342
2257
  }