@poe-platform/safe-fs 0.1.727 → 0.1.729

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 (58) hide show
  1. package/README.md +15 -8
  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 +210 -28
  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 +70 -48
  19. package/dist/safe-fs/fs/memory/index.js +1595 -244
  20. package/dist/safe-fs/fs/memory/ledger.d.ts +8 -1
  21. package/dist/safe-fs/fs/memory/ledger.js +46 -6
  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 +27 -5
  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 +5 -5
  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 +57 -25
  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/fs/webdav/webdav.js +7 -5
  48. package/dist/safe-fs/platform/browser.d.ts +5 -0
  49. package/dist/safe-fs/platform/browser.js +17 -0
  50. package/dist/safe-fs/platform/node.d.ts +5 -0
  51. package/dist/safe-fs/platform/node.js +28 -0
  52. package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
  53. package/dist/safe-fs/platform/transport-budget.js +4 -0
  54. package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
  55. package/dist/safe-fs/testing/object-io-metrics.js +29 -0
  56. package/dist/safe-fs/testing/object-publication.d.ts +3 -1
  57. package/dist/safe-fs/testing/object-publication.js +2 -2
  58. package/package.json +1 -1
@@ -1,19 +1,105 @@
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
+ constructor(mode, ino, now, byteLength, allocation, view) {
19
+ this.mode = mode;
20
+ this.ino = ino;
21
+ this.atimeMs = now;
22
+ this.mtimeMs = now;
23
+ this.ctimeMs = now;
24
+ this.birthtimeMs = now;
25
+ this.byteLength = byteLength;
26
+ this.allocation = allocation;
27
+ this.view = view;
28
+ }
29
+ get data() {
30
+ return this.view ??= (this.byteLength === this.allocation.data.byteLength ? this.allocation.data : this.allocation.data.subarray(0, this.byteLength));
31
+ }
32
+ set data(value) {
33
+ this.view = value;
34
+ this.byteLength = value.byteLength;
35
+ }
36
+ }
37
+ Object.assign(MemoryFileNode.prototype, {
38
+ type: "file",
39
+ revision: 0,
40
+ nlink: 1,
41
+ references: 0,
42
+ });
43
+ const EMPTY_ALLOC_BYTES = new Uint8Array(0);
44
+ const SMALL_ALLOC_SLAB_SIZE = 8192;
45
+ let smallAllocSlab = new Uint8Array(SMALL_ALLOC_SLAB_SIZE);
46
+ let smallAllocOffset = 0;
47
+ class MemoryDirectoryNode {
48
+ constructor(mode, ino, now) {
49
+ this.mode = mode;
50
+ this.ino = ino;
51
+ this.atimeMs = now;
52
+ this.mtimeMs = now;
53
+ this.ctimeMs = now;
54
+ this.birthtimeMs = now;
55
+ this.entries = new Map();
56
+ }
57
+ }
58
+ Object.assign(MemoryDirectoryNode.prototype, {
59
+ type: "directory",
60
+ revision: 0,
61
+ nlink: 1,
62
+ references: 0,
63
+ cachedNlink: 2,
64
+ cachedNlinkRev: 0,
65
+ });
66
+ class MemoryCache {
67
+ allocations = [];
68
+ files = [];
69
+ directories = [];
70
+ removedScratch = [];
71
+ lastFastDirPrefix = "";
72
+ lastFastDirNode;
73
+ lastFastFilePath = "";
74
+ lastFastFileName = "";
75
+ lastFastFileNode;
76
+ clearWrites() {
77
+ this.lastFastDirPrefix = "";
78
+ this.lastFastDirNode = undefined;
79
+ this.lastFastFilePath = "";
80
+ this.lastFastFileName = "";
81
+ this.lastFastFileNode = undefined;
82
+ }
83
+ }
12
84
  const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
85
+ const emptyResolveOptions = Object.freeze({});
86
+ const noFollowResolveOptions = Object.freeze({ followFinal: false });
87
+ const resolvedVoid = Promise.resolve();
13
88
  const preferredIoBlockSize = 64 * 1024;
14
89
  const ext4HtreeEof64 = (1n << 63n) - 1n;
90
+ const extractionStreamGuard = Symbol("extractionStreamGuard");
15
91
  const ownedStats = new WeakMap();
16
92
  const ownedStores = new WeakMap();
93
+ // Ensure V8 uses Tagged representation for timestamp fields so all 4 fields share one HeapNumber pointer.
94
+ {
95
+ const dummyAlloc = new MemoryAllocation(new Uint8Array(0), new MemoryLedger(normalizeMemoryFileSystemLimits({})));
96
+ const dummyFile = new MemoryFileNode(0, 0, null, 0, dummyAlloc, dummyAlloc.data);
97
+ dummyFile.atimeMs = dummyFile.mtimeMs = dummyFile.ctimeMs = dummyFile.birthtimeMs = 1700000000000;
98
+ const dummyDir = new MemoryDirectoryNode(0, 0, null);
99
+ dummyDir.atimeMs = dummyDir.mtimeMs = dummyDir.ctimeMs = dummyDir.birthtimeMs = 1700000000000;
100
+ }
101
+ // Forwarded receivers share the ledger, so mutations invalidate the owner's cache.
102
+ const memoryCaches = new WeakMap();
17
103
  const registeredAuthorities = new WeakSet();
18
104
  const compareOwnedMemory = async (own, peer, options) => {
19
105
  options.signal?.throwIfAborted();
@@ -55,7 +141,123 @@ const compareOwnedMemory = async (own, peer, options) => {
55
141
  options.signal?.throwIfAborted();
56
142
  return qualified ? "distinct" : "unknown";
57
143
  };
144
+ const activeConditionalMutations = new Map();
145
+ export async function bindConditionalMutation(identityScope, binding, action) {
146
+ if (identityScope === undefined || identityScope === null)
147
+ return action();
148
+ activeConditionalMutations.set(identityScope, binding);
149
+ try {
150
+ return await action();
151
+ }
152
+ finally {
153
+ if (activeConditionalMutations.get(identityScope) === binding) {
154
+ activeConditionalMutations.delete(identityScope);
155
+ }
156
+ }
157
+ }
158
+ const STREAM_DONE_RESULT = Object.freeze({ done: true, value: undefined });
159
+ const STREAM_RESOLVED_DONE = Promise.resolve(STREAM_DONE_RESULT);
160
+ class MemoryReadStream {
161
+ constructor(fs, path, options) {
162
+ this.fs = fs;
163
+ this.path = path;
164
+ this.options = options;
165
+ this.abortSignal = options.signal;
166
+ this.initialized = false;
167
+ this.finished = false;
168
+ this.offset = 0;
169
+ this.end = 0;
170
+ this.chunkSize = preferredIoBlockSize;
171
+ this.node = undefined;
172
+ this.allocation = undefined;
173
+ this.data = undefined;
174
+ }
175
+ [Symbol.asyncIterator]() {
176
+ if (this.initialized || this.finished) {
177
+ return new MemoryReadStream(this.fs, this.path, this.options);
178
+ }
179
+ return this;
180
+ }
181
+ _release() {
182
+ if (this.finished)
183
+ return;
184
+ this.finished = true;
185
+ if (this.allocation) {
186
+ this.allocation.release();
187
+ this.allocation = undefined;
188
+ }
189
+ if (this.node) {
190
+ const releasedNode = this.node;
191
+ this.node = undefined;
192
+ this.fs.releaseReference(releasedNode, this.path);
193
+ }
194
+ this.data = undefined;
195
+ }
196
+ tryNextSync() {
197
+ if (this.finished)
198
+ return STREAM_DONE_RESULT;
199
+ try {
200
+ const options = this.options;
201
+ options.signal?.throwIfAborted();
202
+ if (!this.initialized) {
203
+ this.initialized = true;
204
+ const fs = this.fs;
205
+ const path = this.path;
206
+ const start = options.start ?? 0;
207
+ const chunkSize = options.chunkSize ?? preferredIoBlockSize;
208
+ this.chunkSize = chunkSize;
209
+ fs.integer(start, "readStream", path);
210
+ fs.integer(chunkSize, "readStream", path);
211
+ if (chunkSize === 0)
212
+ fs.fail("EINVAL", "readStream", path);
213
+ if (options.endExclusive !== undefined) {
214
+ fs.integer(options.endExclusive, "readStream", path);
215
+ if (options.endExclusive < start)
216
+ fs.fail("EINVAL", "readStream", path);
217
+ }
218
+ const target = fs.file(path, "readStream");
219
+ fs.permission(target, 4, "readStream", path);
220
+ fs.ledger.reserve(path.length * 2, 1, "readStream", path);
221
+ target.references++;
222
+ this.node = target;
223
+ this.allocation = target.allocation;
224
+ target.allocation.retain();
225
+ const data = target.data;
226
+ this.data = data;
227
+ this.offset = start;
228
+ this.end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
229
+ target.atimeMs = Date.now();
230
+ }
231
+ if (this.offset < this.end) {
232
+ const nextEnd = Math.min(this.offset + this.chunkSize, this.end);
233
+ const chunk = this.data.slice(this.offset, nextEnd);
234
+ this.offset = nextEnd;
235
+ return { done: false, value: chunk };
236
+ }
237
+ this._release();
238
+ return STREAM_DONE_RESULT;
239
+ }
240
+ catch (error) {
241
+ this._release();
242
+ throw error;
243
+ }
244
+ }
245
+ next() {
246
+ try {
247
+ const res = this.tryNextSync();
248
+ return res.done ? STREAM_RESOLVED_DONE : Promise.resolve(res);
249
+ }
250
+ catch (error) {
251
+ return Promise.reject(error);
252
+ }
253
+ }
254
+ return() {
255
+ this._release();
256
+ return STREAM_RESOLVED_DONE;
257
+ }
258
+ }
58
259
  export class MemoryFileSystem {
260
+ capabilitiesFor;
59
261
  capabilities = ((filesystem) => {
60
262
  return Object.freeze({
61
263
  read: true, stat: true, readdir: true, realpath: true, access: true, open: true,
@@ -64,12 +266,13 @@ export class MemoryFileSystem {
64
266
  rename: true, atomicRenameNoReplace: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
65
267
  streamingAppend: true, randomAccessWrite: true,
66
268
  readOnly: false,
67
- symlinks: true,
269
+ symlinks: true, conditionalChmod: true,
68
270
  hardlinks: true,
69
271
  permissions: true,
70
272
  timestamps: true,
71
273
  atomicRename: true,
72
- atomicFileStaging: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicTreeRemoval: true,
274
+ atomicFileStaging: true, retainedStagingCleanup: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
275
+ synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
73
276
  atomicDirectoryMetadata: true,
74
277
  streamingRead: true,
75
278
  retainedRead: true,
@@ -83,8 +286,10 @@ export class MemoryFileSystem {
83
286
  ledger;
84
287
  root;
85
288
  totalBytes = 0;
289
+ symlinkCount = 0;
86
290
  constructor(options = {}) {
87
291
  this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
292
+ memoryCaches.set(this.ledger, new MemoryCache());
88
293
  this.ledger.reserve(0, 1, "mkdir", "/");
89
294
  this.root = this.directory(0o755);
90
295
  const root = this.root;
@@ -117,16 +322,28 @@ export class MemoryFileSystem {
117
322
  return [...node.entries.keys()];
118
323
  },
119
324
  }, () => {
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)
325
+ if (Object.getPrototypeOf(this) !== MemoryFileSystem.prototype ||
326
+ this.root !== root ||
327
+ this.ledger !== ownedStores.get(this)?.ledger ||
328
+ this.capabilities !== ownedStores.get(this)?.capabilities) {
124
329
  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
- });
330
+ }
331
+ const proto = MemoryFileSystem.prototype;
332
+ for (let i = 0; i < memoryImplementationKeys.length; i++) {
333
+ const name = memoryImplementationKeys[i];
334
+ const expected = memoryImplementationDescriptors[i];
335
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
336
+ const actual = Object.getOwnPropertyDescriptor(this, name);
337
+ if (!actual || actual.value !== expected.value || actual.get !== expected.get || actual.set !== expected.set)
338
+ return false;
339
+ }
340
+ else if (expected.get !== undefined || expected.set !== undefined || proto[name] !== expected.value) {
341
+ const actual = Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
342
+ if (!actual || actual.value !== expected.value || actual.get !== expected.get || actual.set !== expected.set)
343
+ return false;
344
+ }
345
+ }
346
+ return true;
130
347
  });
131
348
  if (this.compareEntry === memoryImplementation.compareEntry?.value) {
132
349
  registeredAuthorities.add(this);
@@ -138,16 +355,9 @@ export class MemoryFileSystem {
138
355
  }
139
356
  canonicalizeMissingTarget(path, options = {}) {
140
357
  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)
358
+ if (!isStockMemoryMethods(this, missingTargetMethodNames))
144
359
  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
- }
360
+ const owner = ownedStores.get(this);
151
361
  if (path !== "")
152
362
  this.validatePath(path, "realpath");
153
363
  return resolveMissingTarget(owner.root, path || ".", options.signal);
@@ -169,7 +379,8 @@ export class MemoryFileSystem {
169
379
  }
170
380
  retained.set("/", this.root);
171
381
  }
172
- const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional"]);
382
+ const capabilities = this.capabilities;
383
+ const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional", "prepareDirectory", "createStagedFile", "publishStagedFile", "removeStagedFile"]);
173
384
  const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
174
385
  const check = (path, followFinal) => {
175
386
  if (!roots.some(root => root === "/" || path === root || path.startsWith(`${root}/`)))
@@ -193,6 +404,8 @@ export class MemoryFileSystem {
193
404
  get: (target, property) => {
194
405
  if (property === "objects" || property === "confineExtraction")
195
406
  return undefined;
407
+ if (property === "capabilities")
408
+ return capabilities;
196
409
  const value = Reflect.get(target, property);
197
410
  if (typeof value !== "function")
198
411
  return value;
@@ -201,14 +414,22 @@ export class MemoryFileSystem {
201
414
  return (...args) => {
202
415
  if (!allowed.has(String(property)))
203
416
  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.
206
- const paths = property === "symlink" ? [args[1]] : property === "link" || property === "rename" ? args.slice(0, 2) : [args[0]];
417
+ // Stock mutations resolve and commit synchronously. Streamed writes
418
+ // repeat the boundary and file-binding check at each backend mutation.
419
+ const paths = property === "publishStagedFile" ? [args[1], args[0].directory.path] : property === "removeStagedFile" ? [args[0].directory.path] : property === "symlink" ? [args[1]] : property === "link" || property === "rename" ? args.slice(0, 2) : [args[0]];
207
420
  for (const path of paths) {
208
421
  if (typeof path !== "string")
209
422
  throw new FsError("EINVAL");
210
423
  check(path, ["chmod", "utimes", "appendFile", "writeFile", "writeStream", "writeFileConditional", "link"].includes(String(property)));
211
424
  }
425
+ if (property === "writeStream") {
426
+ const path = args[0];
427
+ args[2] = { ...args[2], [extractionStreamGuard]: (node) => {
428
+ check(path, true);
429
+ if (this.resolve(path, "writeStream").node !== node)
430
+ this.fail("EPERM", "writeStream", path);
431
+ } };
432
+ }
212
433
  return Reflect.apply(value, target, args);
213
434
  };
214
435
  },
@@ -222,7 +443,39 @@ export class MemoryFileSystem {
222
443
  };
223
444
  }
224
445
  directory(mode) {
225
- return { ...this.metadata(typeModes.directory | mode), type: "directory", entries: new Map() };
446
+ const now = Date.now();
447
+ const fullMode = typeModes.directory | mode;
448
+ const ino = this.nextInode++;
449
+ const pooled = memoryCaches.get(this.ledger)?.directories.pop();
450
+ if (pooled) {
451
+ pooled.mode = fullMode;
452
+ pooled.ino = ino;
453
+ pooled.nlink = 1;
454
+ pooled.references = 0;
455
+ pooled.revision = 0;
456
+ pooled.atimeMs = now;
457
+ pooled.mtimeMs = now;
458
+ pooled.ctimeMs = now;
459
+ pooled.birthtimeMs = now;
460
+ pooled.cachedNlink = 2;
461
+ pooled.cachedNlinkRev = 0;
462
+ return pooled;
463
+ }
464
+ return new MemoryDirectoryNode(fullMode, ino, now);
465
+ }
466
+ addDirectoryNode(parent, name, mode, syscall, path) {
467
+ const bytes = name.length * 2;
468
+ this.ledger.reserve(bytes, 2, syscall, path);
469
+ try {
470
+ const node = this.directory(mode);
471
+ parent.entries.set(name, node);
472
+ this.changed(parent);
473
+ return node;
474
+ }
475
+ catch (error) {
476
+ this.ledger.release(bytes, 2);
477
+ throw error;
478
+ }
226
479
  }
227
480
  addNode(parent, name, create, syscall, path, retainedBytes = 0) {
228
481
  const bytes = name.length * 2 + retainedBytes;
@@ -230,10 +483,12 @@ export class MemoryFileSystem {
230
483
  try {
231
484
  const node = create();
232
485
  if (node.type === "file")
233
- this.admitSize(undefined, node.data.byteLength, syscall, path);
486
+ this.admitSize(undefined, node.byteLength, syscall, path);
234
487
  parent.entries.set(name, node);
235
488
  if (node.type === "file")
236
- this.totalBytes += node.data.byteLength;
489
+ this.totalBytes += node.byteLength;
490
+ else if (node.type === "symlink")
491
+ this.symlinkCount++;
237
492
  this.changed(parent);
238
493
  return node;
239
494
  }
@@ -245,10 +500,36 @@ export class MemoryFileSystem {
245
500
  releaseNode(node) {
246
501
  if (node.nlink !== 0 || node.references !== 0)
247
502
  return;
503
+ const cache = memoryCaches.get(this.ledger);
248
504
  this.ledger.release(node.type === "symlink" ? node.target.length * 2 : 0, 1);
249
505
  if (node.type === "file") {
250
- this.totalBytes -= node.data.byteLength;
251
- node.allocation.release();
506
+ if (cache.lastFastFileNode === node) {
507
+ cache.lastFastFilePath = "";
508
+ cache.lastFastFileName = "";
509
+ cache.lastFastFileNode = undefined;
510
+ }
511
+ this.totalBytes -= node.byteLength;
512
+ const alloc = node.allocation;
513
+ node.view = undefined;
514
+ alloc.release();
515
+ if (alloc.isReleased64() && cache.allocations.length < 128) {
516
+ cache.allocations.push(alloc);
517
+ }
518
+ if (cache.files.length < 128) {
519
+ cache.files.push(node);
520
+ }
521
+ }
522
+ else if (node.type === "symlink") {
523
+ this.symlinkCount--;
524
+ }
525
+ else {
526
+ if (cache.lastFastDirNode === node) {
527
+ cache.lastFastDirPrefix = "";
528
+ cache.lastFastDirNode = undefined;
529
+ }
530
+ if (node.entries.size === 0 && cache.directories.length < 32) {
531
+ cache.directories.push(node);
532
+ }
252
533
  }
253
534
  }
254
535
  releaseReference(node, path) {
@@ -258,14 +539,16 @@ export class MemoryFileSystem {
258
539
  }
259
540
  replaceData(node, allocation, length = allocation.data.byteLength) {
260
541
  const previous = node.allocation;
261
- this.totalBytes += length - node.data.byteLength;
262
- node.data = allocation.data.subarray(0, length);
542
+ this.totalBytes += length - node.byteLength;
543
+ node.byteLength = length;
544
+ node.view = length === allocation.data.byteLength ? allocation.data : undefined;
263
545
  node.allocation = allocation;
264
546
  previous.release();
265
547
  }
266
548
  admitSize(node, length, syscall, path) {
267
549
  this.ledger.fileSize(length, syscall, path);
268
- if (length - (node?.data.byteLength ?? 0) > (this.ledger.limits.maxBytes ?? Number.MAX_SAFE_INTEGER) - this.totalBytes) {
550
+ const maxBytes = this.ledger.limits.maxBytes;
551
+ if (maxBytes !== undefined && length - (node?.byteLength ?? 0) > maxBytes - this.totalBytes) {
269
552
  this.fail("ENOSPC", syscall, path);
270
553
  }
271
554
  }
@@ -292,20 +575,126 @@ export class MemoryFileSystem {
292
575
  if (!Number.isSafeInteger(value) || value < 0)
293
576
  this.fail("EINVAL", syscall, path);
294
577
  }
295
- changed(node) {
296
- node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
297
- node.mtimeMs = node.ctimeMs = Date.now();
578
+ changed(node, now = Date.now()) {
579
+ node.revision = node.revision < 1073741823 ? (node.revision + 1) | 0 : Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
580
+ node.mtimeMs = node.ctimeMs = now;
581
+ }
582
+ resolveNode(path, syscall, followFinal = true) {
583
+ const cache = memoryCaches.get(this.ledger);
584
+ if (this.symlinkCount === 0 && isCleanAbsolutePath(path)) {
585
+ const fastDir = cache.lastFastDirNode;
586
+ const fastPrefix = cache.lastFastDirPrefix;
587
+ if (fastDir !== undefined && fastDir.nlink !== 0 && fastPrefix.length > 0 && path.startsWith(fastPrefix)) {
588
+ const name = path.slice(fastPrefix.length);
589
+ if (name.length > 0 && !name.includes("/")) {
590
+ this.permission(fastDir, 1, syscall, path);
591
+ if (exceedsComponentByteLimit(name))
592
+ this.fail("ENAMETOOLONG", syscall, path);
593
+ const node = fastDir.entries.get(name);
594
+ if (!node)
595
+ this.fail("ENOENT", syscall, path);
596
+ return node;
597
+ }
598
+ }
599
+ let current = this.root;
600
+ let start = 1;
601
+ while (true) {
602
+ this.permission(current, 1, syscall, path);
603
+ const slash = path.indexOf("/", start);
604
+ if (slash === -1) {
605
+ cache.clearWrites();
606
+ cache.lastFastDirPrefix = path.slice(0, start);
607
+ cache.lastFastDirNode = current;
608
+ const name = path.slice(start);
609
+ if (exceedsComponentByteLimit(name))
610
+ this.fail("ENAMETOOLONG", syscall, path);
611
+ const node = current.entries.get(name);
612
+ if (!node)
613
+ this.fail("ENOENT", syscall, path);
614
+ return node;
615
+ }
616
+ const name = path.slice(start, slash);
617
+ if (exceedsComponentByteLimit(name))
618
+ this.fail("ENAMETOOLONG", syscall, path);
619
+ const next = current.entries.get(name);
620
+ if (!next)
621
+ this.fail("ENOENT", syscall, path);
622
+ if (next.type !== "directory")
623
+ this.fail("ENOTDIR", syscall, path);
624
+ current = next;
625
+ start = slash + 1;
626
+ }
627
+ }
628
+ return this.resolve(path, syscall, followFinal ? emptyResolveOptions : noFollowResolveOptions).node;
298
629
  }
299
- resolve(path, syscall, options = {}) {
630
+ resolve(path, syscall, options = emptyResolveOptions) {
300
631
  this.validatePath(path, syscall);
632
+ if (this.symlinkCount === 0 && options.resizeCreate === undefined
633
+ && options.resolutionSteps === undefined && isCleanAbsolutePath(path)) {
634
+ let current = this.root;
635
+ let start = 1;
636
+ while (true) {
637
+ this.permission(current, 1, syscall, path);
638
+ const slash = path.indexOf("/", start);
639
+ if (slash === -1) {
640
+ const name = path.slice(start);
641
+ if (exceedsComponentByteLimit(name))
642
+ this.fail("ENAMETOOLONG", syscall, path);
643
+ let node = current.entries.get(name);
644
+ if (!node && options.createDirectories !== undefined) {
645
+ this.permission(current, 3, syscall, path);
646
+ node = this.addDirectoryNode(current, name, options.createDirectories, syscall, path);
647
+ }
648
+ if (!node && !options.allowMissing)
649
+ this.fail("ENOENT", syscall, path);
650
+ return { node, parent: current, name, path };
651
+ }
652
+ const name = path.slice(start, slash);
653
+ if (exceedsComponentByteLimit(name))
654
+ this.fail("ENAMETOOLONG", syscall, path);
655
+ let next = current.entries.get(name);
656
+ if (!next && options.createDirectories !== undefined) {
657
+ this.permission(current, 3, syscall, path);
658
+ next = this.addDirectoryNode(current, name, options.createDirectories, syscall, path);
659
+ }
660
+ if (!next)
661
+ this.fail("ENOENT", syscall, path);
662
+ if (next.type !== "directory")
663
+ this.fail("ENOTDIR", syscall, path);
664
+ current = next;
665
+ start = slash + 1;
666
+ }
667
+ }
301
668
  // Bound all component arrays allocated by this resolution, including cycles.
302
669
  let remainingPathUnits = 65_536 - path.length;
303
670
  if (remainingPathUnits < 0)
304
671
  this.fail("ENAMETOOLONG", syscall, path);
672
+ let initialComponents = 0;
673
+ let inComp = false;
674
+ for (let i = 0; i < path.length; i++) {
675
+ if (path.charCodeAt(i) === 47)
676
+ inComp = false;
677
+ else if (!inComp) {
678
+ inComp = true;
679
+ if (++initialComponents > 256)
680
+ this.fail("ENAMETOOLONG", syscall, path);
681
+ }
682
+ }
305
683
  const pending = path.split("/").filter(Boolean);
684
+ let remainingComponents = 256 - pending.length;
306
685
  if (path.endsWith("/"))
307
686
  pending.push("");
308
687
  const stack = [{ node: this.root, name: "" }];
688
+ let recordedUnits = 0;
689
+ const observe = (node, entryPath) => {
690
+ if (!options.resolutionSteps)
691
+ return;
692
+ recordedUnits += entryPath.length + (node.type === "symlink" ? node.target.length : 0);
693
+ if (options.resolutionSteps.length >= 4096 || recordedUnits > 1_048_576)
694
+ this.fail("EFBIG", syscall, path);
695
+ options.resolutionSteps.push({ path: entryPath, stat: this.snapshot(node), ...(node.type === "symlink" ? { linkTarget: node.target } : {}) });
696
+ };
697
+ observe(this.root, "/");
309
698
  let links = 0;
310
699
  while (pending.length > 0) {
311
700
  const component = pending.shift();
@@ -321,7 +710,7 @@ export class MemoryFileSystem {
321
710
  stack.pop();
322
711
  continue;
323
712
  }
324
- if (new TextEncoder().encode(component).byteLength > 255)
713
+ if (exceedsComponentByteLimit(component))
325
714
  this.fail("ENAMETOOLONG", syscall, path);
326
715
  if (options.resizeCreate === true && pending.length === 1 && pending[0] === "")
327
716
  this.fail("EISDIR", syscall, path);
@@ -337,6 +726,8 @@ export class MemoryFileSystem {
337
726
  }
338
727
  this.fail("ENOENT", syscall, path);
339
728
  }
729
+ if (options.resolutionSteps)
730
+ observe(node, [...stack.map(entry => entry.name), component].join("/"));
340
731
  if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
341
732
  if (++links > 40)
342
733
  this.fail("ELOOP", syscall, path);
@@ -344,6 +735,9 @@ export class MemoryFileSystem {
344
735
  this.fail("ENAMETOOLONG", syscall, path);
345
736
  remainingPathUnits -= node.target.length;
346
737
  const target = node.target.split("/").filter(Boolean);
738
+ if (target.length > remainingComponents)
739
+ this.fail("ENAMETOOLONG", syscall, path);
740
+ remainingComponents -= target.length;
347
741
  if (node.target.endsWith("/"))
348
742
  target.push("");
349
743
  if (options.resizeCreate !== undefined && target.at(-1) === "" && pending[0] === "")
@@ -364,6 +758,55 @@ export class MemoryFileSystem {
364
758
  };
365
759
  }
366
760
  file(path, syscall) {
761
+ const cache = memoryCaches.get(this.ledger);
762
+ if (this.symlinkCount === 0 && isCleanAbsolutePath(path)) {
763
+ const fastDir = cache.lastFastDirNode;
764
+ const fastPrefix = cache.lastFastDirPrefix;
765
+ if (fastDir !== undefined && fastDir.nlink !== 0 && fastPrefix.length > 0 && path.startsWith(fastPrefix)) {
766
+ const name = path.slice(fastPrefix.length);
767
+ if (name.length > 0 && !name.includes("/")) {
768
+ this.permission(fastDir, 1, syscall, path);
769
+ if (exceedsComponentByteLimit(name))
770
+ this.fail("ENAMETOOLONG", syscall, path);
771
+ const node = fastDir.entries.get(name);
772
+ if (!node)
773
+ this.fail("ENOENT", syscall, path);
774
+ if (node.type !== "file")
775
+ this.fail("EISDIR", syscall, path);
776
+ return node;
777
+ }
778
+ }
779
+ let current = this.root;
780
+ let start = 1;
781
+ while (true) {
782
+ this.permission(current, 1, syscall, path);
783
+ const slash = path.indexOf("/", start);
784
+ if (slash === -1) {
785
+ cache.clearWrites();
786
+ cache.lastFastDirPrefix = path.slice(0, start);
787
+ cache.lastFastDirNode = current;
788
+ const name = path.slice(start);
789
+ if (exceedsComponentByteLimit(name))
790
+ this.fail("ENAMETOOLONG", syscall, path);
791
+ const node = current.entries.get(name);
792
+ if (!node)
793
+ this.fail("ENOENT", syscall, path);
794
+ if (node.type !== "file")
795
+ this.fail("EISDIR", syscall, path);
796
+ return node;
797
+ }
798
+ const name = path.slice(start, slash);
799
+ if (exceedsComponentByteLimit(name))
800
+ this.fail("ENAMETOOLONG", syscall, path);
801
+ const next = current.entries.get(name);
802
+ if (!next)
803
+ this.fail("ENOENT", syscall, path);
804
+ if (next.type !== "directory")
805
+ this.fail("ENOTDIR", syscall, path);
806
+ current = next;
807
+ start = slash + 1;
808
+ }
809
+ }
367
810
  const node = this.resolve(path, syscall).node;
368
811
  if (node.type !== "file")
369
812
  this.fail("EISDIR", syscall, path);
@@ -377,21 +820,66 @@ export class MemoryFileSystem {
377
820
  return location;
378
821
  }
379
822
  terminalDot(path) {
823
+ const len = path.length;
824
+ if (len === 0)
825
+ return false;
826
+ const last = path.charCodeAt(len - 1);
827
+ if (last !== 46 && last !== 47)
828
+ return false;
380
829
  return /(?:^|\/)\.{1,2}\/*$/.test(path);
381
830
  }
831
+ directoryNlink(node) {
832
+ if (node.cachedNlinkRev === node.revision && node.cachedNlink !== undefined) {
833
+ return node.cachedNlink;
834
+ }
835
+ let nlink = 2;
836
+ node.entries.forEach(entry => {
837
+ if (entry.type === "directory")
838
+ nlink++;
839
+ });
840
+ node.cachedNlinkRev = node.revision;
841
+ node.cachedNlink = nlink;
842
+ return nlink;
843
+ }
382
844
  snapshot(node) {
845
+ const hasRev = Number.isSafeInteger(node.revision);
846
+ const hasScope = ownedStores.get(this)?.intact() === true;
847
+ const size = node.type === "file" ? node.byteLength
848
+ : node.type === "symlink" ? new TextEncoder().encode(node.target).byteLength : 0;
849
+ const nlink = node.type === "directory" && node.nlink !== 0
850
+ ? this.directoryNlink(node) : node.nlink;
851
+ if (hasRev && hasScope) {
852
+ return {
853
+ type: node.type,
854
+ filesystemType: "memory",
855
+ ioBlockSize: preferredIoBlockSize,
856
+ revision: node.revision,
857
+ preferredIoBlockSize: 4096,
858
+ size,
859
+ mode: node.mode,
860
+ identityScope: this.identityScope,
861
+ ino: node.ino,
862
+ dev: 0,
863
+ uid: 0,
864
+ gid: 0,
865
+ nlink,
866
+ atimeMs: node.atimeMs,
867
+ mtimeMs: node.mtimeMs,
868
+ ctimeMs: node.ctimeMs,
869
+ birthtimeMs: node.birthtimeMs,
870
+ };
871
+ }
383
872
  return {
384
873
  type: node.type,
385
874
  filesystemType: "memory",
386
875
  ioBlockSize: preferredIoBlockSize,
387
- ...(Number.isSafeInteger(node.revision) ? { revision: node.revision } : {}),
876
+ ...(hasRev ? { revision: node.revision } : {}),
388
877
  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 } : {}),
878
+ size,
879
+ mode: node.mode,
880
+ ...(hasScope ? { identityScope: this.identityScope } : {}),
392
881
  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,
882
+ nlink,
395
883
  atimeMs: node.atimeMs, mtimeMs: node.mtimeMs, ctimeMs: node.ctimeMs, birthtimeMs: node.birthtimeMs,
396
884
  };
397
885
  }
@@ -409,7 +897,27 @@ export class MemoryFileSystem {
409
897
  allocate(length, syscall, path) {
410
898
  this.ledger.fileSize(length, syscall, path);
411
899
  this.ledger.reserve(length, 0, syscall, path);
900
+ if (length === 0) {
901
+ return new MemoryAllocation(EMPTY_ALLOC_BYTES, this.ledger);
902
+ }
903
+ const allocations = memoryCaches.get(this.ledger).allocations;
904
+ if (length === 64 && allocations.length > 0) {
905
+ const pooled = allocations.pop();
906
+ pooled.reuse();
907
+ return pooled;
908
+ }
412
909
  try {
910
+ // A shared slab's backing buffer exceeds the admitted slice capacity.
911
+ // Finite file/storage ceilings require independently owned exact buffers.
912
+ if (length <= 64 && this.ledger.hasInfiniteRetained && this.ledger.limits.maxFileBytes === Infinity) {
913
+ if (smallAllocOffset + length > SMALL_ALLOC_SLAB_SIZE) {
914
+ smallAllocSlab = new Uint8Array(SMALL_ALLOC_SLAB_SIZE);
915
+ smallAllocOffset = 0;
916
+ }
917
+ const slice = smallAllocSlab.subarray(smallAllocOffset, smallAllocOffset + length);
918
+ smallAllocOffset += length;
919
+ return new MemoryAllocation(slice, this.ledger);
920
+ }
413
921
  return new MemoryAllocation(new Uint8Array(length), this.ledger);
414
922
  }
415
923
  catch (cause) {
@@ -418,9 +926,7 @@ export class MemoryFileSystem {
418
926
  }
419
927
  }
420
928
  open(path, options) {
421
- return openFileDescriptor(path, options, {
422
- noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
423
- }, async (admitted) => {
929
+ return openFileDescriptor(path, options, MEMORY_DESCRIPTOR_CAPABILITIES, async (admitted) => {
424
930
  const location = this.resolve(path, "open", {
425
931
  followFinal: !admitted.noFollow && admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
426
932
  });
@@ -448,40 +954,16 @@ export class MemoryFileSystem {
448
954
  throw error;
449
955
  }
450
956
  node.references++;
451
- const resource = { node, position: 0 };
957
+ const resource = { fs: this, path, append: admitted.append, node, position: 0 };
452
958
  return {
453
959
  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
- },
960
+ getPosition: memoryDescriptorGetPosition,
961
+ stat: memoryDescriptorStat,
962
+ read: memoryDescriptorRead,
963
+ write: memoryDescriptorWrite,
964
+ truncate: memoryDescriptorTruncate,
965
+ sync: memoryDescriptorSync,
966
+ close: memoryDescriptorClose,
485
967
  };
486
968
  });
487
969
  }
@@ -510,7 +992,8 @@ export class MemoryFileSystem {
510
992
  return location.node;
511
993
  return this.addNode(location.parent, location.name, () => {
512
994
  const allocation = new MemoryAllocation(new Uint8Array(), this.ledger);
513
- return { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation };
995
+ const meta = this.metadata(typeModes.file | mode);
996
+ return new MemoryFileNode(meta.mode, meta.ino, meta.atimeMs, 0, allocation, allocation.data);
514
997
  }, syscall, path);
515
998
  }
516
999
  writeData(path, data, options, syscall) {
@@ -518,41 +1001,58 @@ export class MemoryFileSystem {
518
1001
  throw new TypeError("Memory files require Uint8Array data");
519
1002
  const target = this.prepareWrite(path, options, syscall);
520
1003
  const current = target.location.node;
521
- const length = (target.append ? current?.data.byteLength ?? 0 : 0) + data.byteLength;
1004
+ const length = (target.append ? current?.byteLength ?? 0 : 0) + data.byteLength;
522
1005
  this.admitSize(current, length, syscall, path);
523
1006
  const growth = target.append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
524
1007
  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;
1008
+ if (current && target.append) {
1009
+ this.writeAt(current, data, current.byteLength, syscall, path);
1010
+ return current;
1011
+ }
1012
+ if (!current) {
1013
+ const capacity = length > 0 && (target.append || (length < 64 && this.ledger.availableBytes > 65536 && this.ledger.limits.maxFileBytes >= 64))
1014
+ ? Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - target.location.name.length * 2)
1015
+ : length;
1016
+ const allocation = this.allocate(capacity, syscall, path);
1017
+ try {
1018
+ allocation.data.set(data);
1019
+ return this.addNode(target.location.parent, target.location.name, () => {
1020
+ const now = Date.now();
1021
+ const fileMode = typeModes.file | target.mode;
1022
+ const view = capacity === length ? allocation.data : undefined;
1023
+ const pooled = memoryCaches.get(this.ledger).files.pop();
1024
+ if (pooled) {
1025
+ pooled.mode = fileMode;
1026
+ pooled.ino = this.nextInode++;
1027
+ pooled.nlink = 1;
1028
+ pooled.references = 0;
1029
+ pooled.revision = 0;
1030
+ pooled.atimeMs = now;
1031
+ pooled.mtimeMs = now;
1032
+ pooled.ctimeMs = now;
1033
+ pooled.birthtimeMs = now;
1034
+ pooled.byteLength = length;
1035
+ pooled.view = view;
1036
+ pooled.allocation = allocation;
1037
+ return pooled;
1038
+ }
1039
+ return new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view);
1040
+ }, syscall, path);
541
1041
  }
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);
1042
+ catch (error) {
1043
+ allocation.release();
1044
+ throw error;
548
1045
  }
1046
+ }
1047
+ const copied = this.bytes(data, syscall, path);
1048
+ try {
1049
+ this.replaceData(current, copied);
1050
+ this.changed(current);
549
1051
  return current;
550
1052
  }
551
- finally {
552
- if (allocation !== copied && !transferred)
553
- allocation.release();
554
- if (!transferred || allocation !== copied)
555
- copied.release();
1053
+ catch (error) {
1054
+ copied.release();
1055
+ throw error;
556
1056
  }
557
1057
  }
558
1058
  writeAt(node, data, position, syscall, path) {
@@ -562,20 +1062,27 @@ export class MemoryFileSystem {
562
1062
  }
563
1063
  const end = position + data.byteLength;
564
1064
  this.ledger.fileSize(end, syscall, path);
565
- const length = Math.max(node.data.byteLength, end);
1065
+ const curLen = node.byteLength;
1066
+ const length = Math.max(curLen, end);
566
1067
  this.admitSize(node, length, syscall, path);
567
1068
  let allocation = node.allocation;
568
1069
  if (length > allocation.data.byteLength) {
569
1070
  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);
1071
+ const baseCap = curLen === 0 && length >= 8192 && length <= 16384 &&
1072
+ this.ledger.limits.maxFileBytes >= 65536 &&
1073
+ this.ledger.availableBytes >= 65536 &&
1074
+ (this.ledger.limits.maxBytes === undefined || this.ledger.limits.maxBytes - this.totalBytes >= 65536)
1075
+ ? 65536
1076
+ : Math.max(length, curLen * 2, 64);
1077
+ const capacity = Math.min(baseCap, this.ledger.limits.maxFileBytes, this.ledger.availableBytes);
571
1078
  allocation = this.allocate(capacity, syscall, path);
572
1079
  }
573
1080
  try {
574
1081
  const storage = allocation.data;
575
1082
  if (allocation !== node.allocation)
576
- storage.set(node.data);
577
- if (position > node.data.byteLength)
578
- storage.fill(0, node.data.byteLength, position);
1083
+ storage.set(node.view ?? (curLen === node.allocation.data.byteLength ? node.allocation.data : node.allocation.data.subarray(0, curLen)));
1084
+ if (position > curLen)
1085
+ storage.fill(0, curLen, position);
579
1086
  storage.set(data, position);
580
1087
  }
581
1088
  catch (error) {
@@ -586,11 +1093,313 @@ export class MemoryFileSystem {
586
1093
  if (allocation !== node.allocation)
587
1094
  this.replaceData(node, allocation, length);
588
1095
  else {
589
- this.totalBytes += length - node.data.byteLength;
590
- node.data = allocation.data.subarray(0, length);
1096
+ this.totalBytes += length - curLen;
1097
+ node.byteLength = length;
1098
+ node.view = length === allocation.data.byteLength ? allocation.data : undefined;
591
1099
  }
592
1100
  this.changed(node);
593
1101
  }
1102
+ writeMemoryFileFast(path, data, append, mode) {
1103
+ const cache = memoryCaches.get(this.ledger);
1104
+ const syscall = append ? "appendFile" : "writeFile";
1105
+ if (append &&
1106
+ path === cache.lastFastFilePath &&
1107
+ cache.lastFastFileNode !== undefined &&
1108
+ cache.lastFastFileNode.nlink !== 0 &&
1109
+ cache.lastFastDirNode !== undefined &&
1110
+ cache.lastFastDirNode.nlink !== 0 &&
1111
+ (cache.lastFastDirNode.mode & 1) !== 0 &&
1112
+ (cache.lastFastFileNode.mode & 2) !== 0) {
1113
+ const current = cache.lastFastFileNode;
1114
+ const length = current.byteLength + data.byteLength;
1115
+ this.admitSize(current, length, syscall, path);
1116
+ const growth = length > current.allocation.data.byteLength ? length : 0;
1117
+ this.ledger.check(data.byteLength + growth, 0, syscall, path);
1118
+ this.writeAt(current, data, current.byteLength, syscall, path);
1119
+ return;
1120
+ }
1121
+ let parent = this.root;
1122
+ let start = 1;
1123
+ let name = "";
1124
+ const cachedPrefix = cache.lastFastDirPrefix;
1125
+ const cachedDir = cache.lastFastDirNode;
1126
+ if (cachedDir !== undefined &&
1127
+ cachedDir.nlink !== 0 &&
1128
+ (cachedDir.mode & 1) !== 0 &&
1129
+ cachedPrefix.length > 1 &&
1130
+ path.startsWith(cachedPrefix) &&
1131
+ path.indexOf("/", cachedPrefix.length) === -1) {
1132
+ parent = cachedDir;
1133
+ name = path.slice(cachedPrefix.length);
1134
+ if (exceedsComponentByteLimit(name))
1135
+ this.fail("ENAMETOOLONG", syscall, path);
1136
+ }
1137
+ else {
1138
+ while (true) {
1139
+ this.permission(parent, 1, syscall, path);
1140
+ const slash = path.indexOf("/", start);
1141
+ if (slash === -1) {
1142
+ name = path.slice(start);
1143
+ if (exceedsComponentByteLimit(name))
1144
+ this.fail("ENAMETOOLONG", syscall, path);
1145
+ if (start > 1 && (parent.mode & 1) !== 0) {
1146
+ cache.lastFastDirPrefix = path.slice(0, start);
1147
+ cache.lastFastDirNode = parent;
1148
+ }
1149
+ break;
1150
+ }
1151
+ const seg = path.slice(start, slash);
1152
+ if (exceedsComponentByteLimit(seg))
1153
+ this.fail("ENAMETOOLONG", syscall, path);
1154
+ const next = parent.entries.get(seg);
1155
+ if (!next)
1156
+ this.fail("ENOENT", syscall, path);
1157
+ if (next.type !== "directory")
1158
+ this.fail("ENOTDIR", syscall, path);
1159
+ parent = next;
1160
+ start = slash + 1;
1161
+ }
1162
+ }
1163
+ const existing = parent.entries.get(name);
1164
+ if (existing) {
1165
+ if (existing.type !== "file")
1166
+ this.fail("EISDIR", syscall, path);
1167
+ this.permission(existing, 2, syscall, path);
1168
+ }
1169
+ else {
1170
+ this.permission(parent, 3, syscall, path);
1171
+ }
1172
+ const current = existing;
1173
+ const length = (append ? current?.byteLength ?? 0 : 0) + data.byteLength;
1174
+ this.admitSize(current, length, syscall, path);
1175
+ const growth = append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
1176
+ const nameBytes = name.length * 2;
1177
+ this.ledger.check(data.byteLength + growth + (current ? 0 : nameBytes), current ? 0 : 2, syscall, path);
1178
+ if (current && append) {
1179
+ this.writeAt(current, data, current.byteLength, syscall, path);
1180
+ cache.lastFastFilePath = path;
1181
+ cache.lastFastFileName = name;
1182
+ cache.lastFastFileNode = current;
1183
+ return;
1184
+ }
1185
+ if (!current) {
1186
+ const capacity = length;
1187
+ const allocation = this.allocate(capacity, syscall, path);
1188
+ try {
1189
+ allocation.data.set(data);
1190
+ this.ledger.reserve(nameBytes, 2, syscall, path);
1191
+ try {
1192
+ const now = Date.now();
1193
+ const fileMode = typeModes.file | mode;
1194
+ const view = allocation.data;
1195
+ let node = cache.files.pop();
1196
+ if (node) {
1197
+ node.mode = fileMode;
1198
+ node.ino = this.nextInode++;
1199
+ node.nlink = 1;
1200
+ node.references = 0;
1201
+ node.revision = 0;
1202
+ node.atimeMs = now;
1203
+ node.mtimeMs = now;
1204
+ node.ctimeMs = now;
1205
+ node.birthtimeMs = now;
1206
+ node.byteLength = length;
1207
+ node.view = view;
1208
+ node.allocation = allocation;
1209
+ }
1210
+ else {
1211
+ node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view);
1212
+ }
1213
+ const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
1214
+ parent.entries.set(name, node);
1215
+ this.totalBytes += length;
1216
+ this.changed(parent, now);
1217
+ if (prevNlink !== undefined) {
1218
+ parent.cachedNlink = prevNlink;
1219
+ parent.cachedNlinkRev = parent.revision;
1220
+ }
1221
+ cache.lastFastFilePath = path;
1222
+ cache.lastFastFileName = name;
1223
+ cache.lastFastFileNode = node;
1224
+ }
1225
+ catch (error) {
1226
+ this.ledger.release(nameBytes, 2);
1227
+ throw error;
1228
+ }
1229
+ }
1230
+ catch (error) {
1231
+ allocation.release();
1232
+ throw error;
1233
+ }
1234
+ return;
1235
+ }
1236
+ const copied = this.bytes(data, syscall, path);
1237
+ try {
1238
+ this.replaceData(current, copied);
1239
+ this.changed(current);
1240
+ cache.lastFastFilePath = path;
1241
+ cache.lastFastFileName = name;
1242
+ cache.lastFastFileNode = current;
1243
+ }
1244
+ catch (error) {
1245
+ copied.release();
1246
+ throw error;
1247
+ }
1248
+ }
1249
+ writeMemoryFileInDirFast(dirPrefix, name, data, append, mode) {
1250
+ const cache = memoryCaches.get(this.ledger);
1251
+ const syscall = append ? "appendFile" : "writeFile";
1252
+ try {
1253
+ if (append &&
1254
+ dirPrefix === cache.lastFastDirPrefix &&
1255
+ name === cache.lastFastFileName &&
1256
+ cache.lastFastFileNode !== undefined &&
1257
+ cache.lastFastFileNode.nlink !== 0 &&
1258
+ cache.lastFastDirNode !== undefined &&
1259
+ cache.lastFastDirNode.nlink !== 0 &&
1260
+ (cache.lastFastDirNode.mode & 1) !== 0 &&
1261
+ (cache.lastFastFileNode.mode & 2) !== 0) {
1262
+ const current = cache.lastFastFileNode;
1263
+ const length = current.byteLength + data.byteLength;
1264
+ this.admitSize(current, length, syscall, name);
1265
+ const growth = length > current.allocation.data.byteLength ? length : 0;
1266
+ this.ledger.check(data.byteLength + growth, 0, syscall, name);
1267
+ this.writeAt(current, data, current.byteLength, syscall, name);
1268
+ return;
1269
+ }
1270
+ let parent = this.root;
1271
+ const cachedDir = cache.lastFastDirNode;
1272
+ if (cachedDir !== undefined &&
1273
+ cachedDir.nlink !== 0 &&
1274
+ (cachedDir.mode & 1) !== 0 &&
1275
+ dirPrefix === cache.lastFastDirPrefix) {
1276
+ parent = cachedDir;
1277
+ if (exceedsComponentByteLimit(name))
1278
+ this.fail("ENAMETOOLONG", syscall, name);
1279
+ }
1280
+ else {
1281
+ let start = 1;
1282
+ while (start < dirPrefix.length) {
1283
+ this.permission(parent, 1, syscall, name);
1284
+ const slash = dirPrefix.indexOf("/", start);
1285
+ const seg = slash === -1 ? dirPrefix.slice(start) : dirPrefix.slice(start, slash);
1286
+ if (exceedsComponentByteLimit(seg))
1287
+ this.fail("ENAMETOOLONG", syscall, name);
1288
+ const next = parent.entries.get(seg);
1289
+ if (!next)
1290
+ this.fail("ENOENT", syscall, name);
1291
+ if (next.type !== "directory")
1292
+ this.fail("ENOTDIR", syscall, name);
1293
+ parent = next;
1294
+ if (slash === -1)
1295
+ break;
1296
+ start = slash + 1;
1297
+ }
1298
+ this.permission(parent, 1, syscall, name);
1299
+ if (exceedsComponentByteLimit(name))
1300
+ this.fail("ENAMETOOLONG", syscall, name);
1301
+ if (dirPrefix.length > 1 && (parent.mode & 1) !== 0) {
1302
+ cache.lastFastDirPrefix = dirPrefix;
1303
+ cache.lastFastDirNode = parent;
1304
+ }
1305
+ }
1306
+ const existing = parent.entries.get(name);
1307
+ if (existing) {
1308
+ if (existing.type !== "file")
1309
+ this.fail("EISDIR", syscall, name);
1310
+ this.permission(existing, 2, syscall, name);
1311
+ }
1312
+ else {
1313
+ this.permission(parent, 3, syscall, name);
1314
+ }
1315
+ const current = existing;
1316
+ const length = (append ? current?.byteLength ?? 0 : 0) + data.byteLength;
1317
+ this.admitSize(current, length, syscall, name);
1318
+ const growth = append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
1319
+ const nameBytes = name.length * 2;
1320
+ this.ledger.check(data.byteLength + growth + (current ? 0 : nameBytes), current ? 0 : 2, syscall, name);
1321
+ if (current && append) {
1322
+ this.writeAt(current, data, current.byteLength, syscall, name);
1323
+ cache.lastFastFilePath = "";
1324
+ cache.lastFastFileName = name;
1325
+ cache.lastFastFileNode = current;
1326
+ return;
1327
+ }
1328
+ if (!current) {
1329
+ const capacity = length;
1330
+ const allocation = this.allocate(capacity, syscall, name);
1331
+ try {
1332
+ allocation.data.set(data);
1333
+ this.ledger.reserve(nameBytes, 2, syscall, name);
1334
+ try {
1335
+ const now = Date.now();
1336
+ const fileMode = typeModes.file | mode;
1337
+ const view = allocation.data;
1338
+ let node = cache.files.pop();
1339
+ if (node) {
1340
+ node.mode = fileMode;
1341
+ node.ino = this.nextInode++;
1342
+ node.nlink = 1;
1343
+ node.references = 0;
1344
+ node.revision = 0;
1345
+ node.atimeMs = now;
1346
+ node.mtimeMs = now;
1347
+ node.ctimeMs = now;
1348
+ node.birthtimeMs = now;
1349
+ node.byteLength = length;
1350
+ node.view = view;
1351
+ node.allocation = allocation;
1352
+ }
1353
+ else {
1354
+ node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view);
1355
+ }
1356
+ const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
1357
+ parent.entries.set(name, node);
1358
+ this.totalBytes += length;
1359
+ this.changed(parent, now);
1360
+ if (prevNlink !== undefined) {
1361
+ parent.cachedNlink = prevNlink;
1362
+ parent.cachedNlinkRev = parent.revision;
1363
+ }
1364
+ cache.lastFastFilePath = "";
1365
+ cache.lastFastFileName = name;
1366
+ cache.lastFastFileNode = node;
1367
+ }
1368
+ catch (error) {
1369
+ this.ledger.release(nameBytes, 2);
1370
+ throw error;
1371
+ }
1372
+ }
1373
+ catch (error) {
1374
+ allocation.release();
1375
+ throw error;
1376
+ }
1377
+ return;
1378
+ }
1379
+ const copied = this.bytes(data, syscall, name);
1380
+ try {
1381
+ this.replaceData(current, copied);
1382
+ this.changed(current);
1383
+ cache.lastFastFilePath = "";
1384
+ cache.lastFastFileName = name;
1385
+ cache.lastFastFileNode = current;
1386
+ }
1387
+ catch (error) {
1388
+ copied.release();
1389
+ throw error;
1390
+ }
1391
+ }
1392
+ catch (error) {
1393
+ if (error instanceof FsError && error.path === name) {
1394
+ throw new FsError(error.code, {
1395
+ syscall: error.syscall ?? syscall,
1396
+ path: dirPrefix + name,
1397
+ ...(error.dest === undefined ? {} : { dest: error.dest }),
1398
+ });
1399
+ }
1400
+ throw error;
1401
+ }
1402
+ }
594
1403
  async readFile(path, options = {}) {
595
1404
  options.signal?.throwIfAborted();
596
1405
  if (options.maxBytes !== undefined)
@@ -610,19 +1419,26 @@ export class MemoryFileSystem {
610
1419
  }
611
1420
  if (!node)
612
1421
  this.fail("EAGAIN", "fileStaging", path);
1422
+ if (!unchanged) {
1423
+ const currentScope = ownedStores.get(this)?.intact() ? this.identityScope : undefined;
1424
+ if (!expected.identityScope || expected.identityScope !== currentScope
1425
+ || !Number.isSafeInteger(expected.ino) || !Number.isSafeInteger(expected.dev))
1426
+ this.fail("ENOTSUP", "fileStaging", path);
1427
+ if (node.ino !== expected.ino || expected.dev !== 0 || node.type !== expected.type)
1428
+ this.fail("EAGAIN", "fileStaging", path);
1429
+ return;
1430
+ }
613
1431
  const current = this.snapshot(node);
614
1432
  if (!expected.identityScope || expected.identityScope !== current.identityScope
615
1433
  || !Number.isSafeInteger(expected.ino) || !Number.isSafeInteger(expected.dev))
616
1434
  this.fail("ENOTSUP", "fileStaging", path);
617
1435
  if (current.ino !== expected.ino || current.dev !== expected.dev || current.type !== expected.type)
618
1436
  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
- }
1437
+ if (!Number.isSafeInteger(expected.revision) || !Number.isSafeInteger(current.revision))
1438
+ this.fail("ENOTSUP", "fileStaging", path);
1439
+ if (current.revision !== expected.revision || current.size !== expected.size || current.mode !== expected.mode
1440
+ || current.nlink !== expected.nlink || current.mtimeMs !== expected.mtimeMs || current.ctimeMs !== expected.ctimeMs)
1441
+ this.fail("EAGAIN", "fileStaging", path);
626
1442
  }
627
1443
  stagingLocations(staging) {
628
1444
  const parent = this.entry(staging.parent.path, "fileStaging");
@@ -639,7 +1455,10 @@ export class MemoryFileSystem {
639
1455
  return { directory, file };
640
1456
  }
641
1457
  async createStagedFile(directoryPath, name, content, options) {
1458
+ options = snapshotStagingCreation(options, directoryPath);
642
1459
  options.signal?.throwIfAborted();
1460
+ if (options.retainCleanup === true && this.capabilities.retainedStagingCleanup !== true)
1461
+ this.fail("ENOTSUP", "createStagedFile", directoryPath);
643
1462
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
644
1463
  this.fail("EINVAL", "createStagedFile", directoryPath);
645
1464
  const location = this.entry(directoryPath, "createStagedFile", true);
@@ -658,7 +1477,11 @@ export class MemoryFileSystem {
658
1477
  if (content.type === "symlink")
659
1478
  this.validatePath(content.target, "createStagedFile");
660
1479
  const retained = (location.name.length + name.length) * 2 + (content.type === "symlink" ? content.target.length * 2 : 0);
661
- this.ledger.reserve(retained, 4, "createStagedFile", directoryPath);
1480
+ const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
1481
+ const filePath = `${location.path}/${name}`;
1482
+ const cleanupBytes = options.retainCleanup ? (parentPath.length + location.path.length + filePath.length) * 2 : 0;
1483
+ const cleanupUnits = options.retainCleanup ? 3 : 0;
1484
+ this.ledger.reserve(retained + cleanupBytes, 4 + cleanupUnits, "createStagedFile", directoryPath);
662
1485
  let allocation;
663
1486
  let directory;
664
1487
  let file;
@@ -667,7 +1490,7 @@ export class MemoryFileSystem {
667
1490
  allocation = this.bytes(content.data, "createStagedFile", directoryPath);
668
1491
  directory = this.directory(0o700);
669
1492
  file = content.type === "file"
670
- ? { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation: allocation }
1493
+ ? (() => { const m = this.metadata(typeModes.file | mode); return new MemoryFileNode(m.mode, m.ino, m.atimeMs, allocation.data.byteLength, allocation, allocation.data); })()
671
1494
  : { ...this.metadata(typeModes.symlink | mode), type: "symlink", target: content.target };
672
1495
  if (options.atimeMs !== undefined)
673
1496
  file.atimeMs = options.atimeMs;
@@ -677,52 +1500,153 @@ export class MemoryFileSystem {
677
1500
  location.parent.entries.set(location.name, directory);
678
1501
  if (file.type === "file")
679
1502
  this.totalBytes += file.data.byteLength;
1503
+ else
1504
+ this.symlinkCount++;
680
1505
  }
681
1506
  catch (error) {
682
1507
  allocation?.release();
683
- this.ledger.release(retained, 4);
1508
+ this.ledger.release(retained + cleanupBytes, 4 + cleanupUnits);
684
1509
  throw error;
685
1510
  }
686
1511
  this.changed(location.parent);
687
- const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
688
1512
  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) });
1513
+ const staging = Object.freeze({ parent: receipt(parentPath, location.parent), directory: receipt(location.path, directory), file: receipt(filePath, file) });
1514
+ if (!options.retainCleanup)
1515
+ return staging;
1516
+ // Reservations above include these references before either entry is exposed.
1517
+ location.parent.references++;
1518
+ directory.references++;
1519
+ file.references++;
1520
+ return Object.freeze({ ...staging, cleanup: this.retainStagingCleanup(staging, location.parent, directory, file, location.name, name) });
1521
+ }
1522
+ retainStagingCleanup(staging, parent, directory, file, directoryName, fileName) {
1523
+ // Clear this state on release, so keeping the closed handle does not keep
1524
+ // detached nodes alive outside the Memory ledger.
1525
+ let retained = { parent, directory, file };
1526
+ return createStagingCleanup(staging.directory.path, options => {
1527
+ options.signal?.throwIfAborted();
1528
+ const state = retained;
1529
+ if (state.parent.nlink === 0 || state.parent.entries.get(directoryName) !== state.directory)
1530
+ this.fail("EAGAIN", "removeStagedFile", staging.directory.path);
1531
+ this.expectEntry(state.parent, staging.parent.stat, staging.parent.path, false);
1532
+ this.expectEntry(state.directory, staging.directory.stat, staging.directory.path, false);
1533
+ if ((state.directory.mode & 0o777) !== 0o700)
1534
+ this.fail("EAGAIN", "removeStagedFile", staging.directory.path);
1535
+ const child = state.directory.entries.get(fileName);
1536
+ if (child && child !== state.file)
1537
+ this.fail("EAGAIN", "removeStagedFile", staging.file.path);
1538
+ this.removeStagingLocations(staging, {
1539
+ node: state.directory, parent: state.parent, name: directoryName, path: staging.directory.path,
1540
+ }, { node: child, parent: state.directory, name: fileName, path: staging.file.path });
1541
+ }, () => {
1542
+ const state = retained;
1543
+ retained = undefined;
1544
+ this.releaseReference(state.file, staging.file.path);
1545
+ this.releaseReference(state.directory, staging.directory.path);
1546
+ this.releaseReference(state.parent, staging.parent.path);
1547
+ });
690
1548
  }
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);
1549
+ async prepareDirectoryAncestry(ancestors, options = {}) {
1550
+ const entries = snapshotDirectoryAncestry(ancestors);
1551
+ const controls = options.signal === undefined ? {} : { signal: options.signal };
1552
+ controls.signal?.throwIfAborted();
1553
+ return () => this.verifyDirectoryAncestry(entries, controls);
1554
+ }
1555
+ async prepareStagingResolution(path, options = {}) {
1556
+ const controls = options.signal === undefined ? {} : { signal: options.signal };
1557
+ controls.signal?.throwIfAborted();
1558
+ this.validatePath(path, "prepareStagingResolution");
1559
+ if (!path.startsWith("/") || normalizePath(path) !== path)
1560
+ this.fail("EINVAL", "prepareStagingResolution", path);
1561
+ const traversed = [];
1562
+ const location = this.resolve(path, "prepareStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: traversed });
1563
+ if (location.node && location.node.type !== "file")
1564
+ this.fail("ENOTSUP", "prepareStagingResolution", path);
1565
+ const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
1566
+ const ancestors = snapshotDirectoryAncestry(directoryAncestryPaths(parentPath).map(entryPath => ({
1567
+ path: entryPath, stat: this.snapshot(this.entry(entryPath, "prepareStagingResolution").node),
1568
+ })));
1569
+ const steps = traversed.map(entry => ({ ...entry, stat: { ...entry.stat } }));
1570
+ const canonical = location.path;
1571
+ const destination = location.node ? this.snapshot(location.node) : null;
1572
+ const validate = () => {
1573
+ controls.signal?.throwIfAborted();
1574
+ const currentSteps = [];
1575
+ let current;
1576
+ try {
1577
+ current = this.resolve(path, "verifyStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: currentSteps });
1578
+ }
1579
+ catch (error) {
1580
+ if (error instanceof FsError && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code))
1581
+ this.fail("EAGAIN", "verifyStagingResolution", path);
1582
+ throw error;
710
1583
  }
1584
+ if (current.path !== canonical || steps.length !== currentSteps.length)
1585
+ this.fail("EAGAIN", "verifyStagingResolution", path);
1586
+ for (const [index, before] of steps.entries()) {
1587
+ const after = currentSteps[index];
1588
+ if (before.path !== after.path || before.linkTarget !== after.linkTarget || before.stat.type !== after.stat.type
1589
+ || compareIdentity(before.stat, after.stat) !== "same"
1590
+ || before.stat.type !== "directory" && ["revision", "size", "mode", "nlink", "mtimeMs", "ctimeMs"].some(field => before.stat[field] !== after.stat[field])) {
1591
+ this.fail("EAGAIN", "verifyStagingResolution", before.path);
1592
+ }
1593
+ }
1594
+ this.expectEntry(current.node, destination, path);
1595
+ return this.verifyDirectoryAncestry(ancestors, controls);
1596
+ };
1597
+ return snapshotStagingResolution({ path: canonical, parent: this.snapshot(location.parent), destination, ancestors, traversed: steps, validate });
1598
+ }
1599
+ verifyDirectoryAncestry(ancestors, options) {
1600
+ const entries = snapshotDirectoryAncestry(ancestors);
1601
+ options.signal?.throwIfAborted();
1602
+ let node = this.root;
1603
+ for (const [index, expected] of entries.entries()) {
1604
+ if (index > 0)
1605
+ node = node.entries.get(expected.path.slice(expected.path.lastIndexOf("/") + 1));
1606
+ if (node?.type !== "directory")
1607
+ this.fail("EAGAIN", "verifyDirectoryAncestry", expected.path);
1608
+ this.expectEntry(node, expected.stat, expected.path, false);
1609
+ this.permission(node, 1, "verifyDirectoryAncestry", expected.path);
711
1610
  }
1611
+ return true;
1612
+ }
1613
+ async publishStagedFile(receipt, destination, options) {
1614
+ const signal = options.signal;
1615
+ const guard = options.commitGuard;
1616
+ const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
1617
+ const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
1618
+ const staging = { parent: snapshot(receipt.parent), directory: snapshot(receipt.directory), file: snapshot(receipt.file) };
1619
+ const parent = { ...options.parent };
1620
+ const expected = options.destination === null ? null : { ...options.destination };
1621
+ const parentPath = destination.slice(0, destination.lastIndexOf("/")) || "/";
1622
+ if (ancestors && ancestors.at(-1)?.path !== parentPath)
1623
+ this.fail("EINVAL", "publishStagedFile", destination);
1624
+ const controls = signal === undefined ? {} : { signal };
1625
+ signal?.throwIfAborted();
1626
+ if (guard !== undefined)
1627
+ runStagingGuard(guard);
1628
+ signal?.throwIfAborted();
1629
+ // All inputs were copied before the guard. Neither outer validation nor
1630
+ // these local checks yield before the stock rename commits.
1631
+ if (ancestors)
1632
+ runStagingGuard(() => this.verifyDirectoryAncestry(ancestors, controls));
712
1633
  const { directory, file } = this.stagingLocations(staging);
713
1634
  this.expectEntry(file.node, staging.file.stat, staging.file.path);
714
1635
  const target = this.entry(destination, "publishStagedFile", true);
715
- this.expectEntry(target.parent, options.parent, destination, false);
716
- this.expectEntry(target.node, options.destination, destination);
1636
+ this.expectEntry(target.parent, parent, destination, false);
1637
+ this.expectEntry(target.node, expected, destination);
717
1638
  if (target.parent === directory.node || target.node === directory.node || target.node === file.node)
718
1639
  this.fail("EINVAL", "publishStagedFile", destination);
719
- if (target.node && (target.node.type !== "file" || target.node.nlink !== 1))
1640
+ if (target.node && target.node.type !== "file")
720
1641
  this.fail("EAGAIN", "publishStagedFile", destination);
721
- return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...options, noReplace: options.destination === null });
1642
+ return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
722
1643
  }
723
1644
  async removeStagedFile(staging, options = {}) {
724
1645
  options.signal?.throwIfAborted();
725
1646
  const { directory, file } = this.stagingLocations(staging);
1647
+ this.removeStagingLocations(staging, directory, file);
1648
+ }
1649
+ removeStagingLocations(staging, directory, file) {
726
1650
  if (file.node)
727
1651
  this.expectEntry(file.node, staging.file.stat, staging.file.path);
728
1652
  const node = directory.node;
@@ -762,8 +1686,10 @@ export class MemoryFileSystem {
762
1686
  }
763
1687
  if (node.type !== "directory")
764
1688
  this.fail("ENOTDIR", "prepareDirectory", path);
765
- if (mode !== undefined)
1689
+ if (mode !== undefined) {
766
1690
  node.mode = typeModes.directory | mode;
1691
+ memoryCaches.get(this.ledger).clearWrites();
1692
+ }
767
1693
  if (options.atimeMs !== undefined)
768
1694
  node.atimeMs = options.atimeMs;
769
1695
  if (options.mtimeMs !== undefined)
@@ -779,11 +1705,20 @@ export class MemoryFileSystem {
779
1705
  this.expectEntry(location.node, options.expected, path);
780
1706
  if (location.node && location.node.type !== "file")
781
1707
  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)));
1708
+ if (options.atimeMs !== undefined)
1709
+ this.integer(options.atimeMs, "writeFileConditional", path);
1710
+ if (options.mtimeMs !== undefined)
1711
+ this.integer(options.mtimeMs, "writeFileConditional", path);
1712
+ const node = this.writeData(path, data, { ...(options.mode === undefined ? {} : { mode: options.mode }), flag: options.append ? "a" : "w" }, "writeFileConditional");
1713
+ if (options.atimeMs !== undefined)
1714
+ node.atimeMs = options.atimeMs;
1715
+ if (options.mtimeMs !== undefined)
1716
+ node.mtimeMs = options.mtimeMs;
1717
+ return Object.freeze(this.snapshot(node));
784
1718
  }
785
1719
  async removeEntryConditional(path, options) {
786
1720
  options.signal?.throwIfAborted();
1721
+ const returnRemainingStat = options.returnRemainingStat === true;
787
1722
  const location = this.entry(path, "removeEntryConditional", true);
788
1723
  this.expectEntry(location.parent, options.parent, path, false);
789
1724
  this.expectEntry(location.node, options.expected, path);
@@ -798,12 +1733,15 @@ export class MemoryFileSystem {
798
1733
  if (node.type === "directory" && node.entries.size > 0)
799
1734
  this.fail("ENOTEMPTY", "removeEntryConditional", path);
800
1735
  location.parent.entries.delete(location.name);
1736
+ memoryCaches.get(this.ledger).clearWrites();
801
1737
  this.ledger.release(location.name.length * 2, 1);
802
1738
  node.nlink--;
803
1739
  node.ctimeMs = Date.now();
804
1740
  node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
805
1741
  this.releaseNode(node);
806
1742
  this.changed(location.parent);
1743
+ if (returnRemainingStat)
1744
+ return Object.freeze(this.snapshot(node));
807
1745
  }
808
1746
  async removeFileConditional(path, options) {
809
1747
  options.signal?.throwIfAborted();
@@ -815,6 +1753,7 @@ export class MemoryFileSystem {
815
1753
  this.fail("EINVAL", "removeFileConditional", path);
816
1754
  this.permission(location.parent, 3, "removeFileConditional", path);
817
1755
  location.parent.entries.delete(location.name);
1756
+ memoryCaches.get(this.ledger).clearWrites();
818
1757
  this.ledger.release(location.name.length * 2, 1);
819
1758
  node.nlink--;
820
1759
  node.ctimeMs = Date.now();
@@ -841,40 +1780,50 @@ export class MemoryFileSystem {
841
1780
  }
842
1781
  async stat(path, options = {}) {
843
1782
  options.signal?.throwIfAborted();
844
- return this.snapshot(this.resolve(path, "stat").node);
1783
+ return this.snapshot(this.resolveNode(path, "stat", true));
845
1784
  }
846
1785
  async lstat(path, options = {}) {
847
1786
  options.signal?.throwIfAborted();
848
- const stat = this.snapshot(this.resolve(path, "lstat", { followFinal: false }).node);
1787
+ const stat = this.snapshot(this.resolveNode(path, "lstat", false));
849
1788
  ownedStats.set(stat, { filesystem: this, path, root: this.root });
850
1789
  return stat;
851
1790
  }
852
1791
  async readdir(path, options = {}) {
853
1792
  const limit = directoryEntryLimit(options, path);
854
- const node = this.resolve(path, "readdir").node;
1793
+ const node = this.resolveNode(path, "readdir", true);
855
1794
  if (node.type !== "directory")
856
1795
  this.fail("ENOTDIR", "readdir", path);
857
1796
  this.permission(node, 4, "readdir", path);
858
1797
  admitDirectoryEntries(node.entries.size, limit, path);
859
1798
  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);
1799
+ const result = new Array(node.entries.size);
1800
+ let idx = 0;
1801
+ for (const [name, entry] of node.entries) {
1802
+ result[idx++] = { name, type: entry.type };
1803
+ }
1804
+ return result.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
862
1805
  }
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")
1806
+ mkdir(path, options = {}) {
1807
+ try {
1808
+ options.signal?.throwIfAborted();
1809
+ this.validatePath(path, "mkdir");
1810
+ const mode = this.mode(options.mode, 0o777, "mkdir", path);
1811
+ if (options.recursive) {
1812
+ const node = this.resolve(path, "mkdir", { createDirectories: mode }).node;
1813
+ if (node.type !== "directory")
1814
+ this.fail("EEXIST", "mkdir", path);
1815
+ return resolvedVoid;
1816
+ }
1817
+ const location = this.resolve(path.replace(/\/+$/, "") || "/", "mkdir", { allowMissing: true, followFinal: false });
1818
+ if (location.node)
870
1819
  this.fail("EEXIST", "mkdir", path);
871
- return;
1820
+ this.permission(location.parent, 3, "mkdir", path);
1821
+ this.addNode(location.parent, location.name, () => this.directory(mode), "mkdir", path);
1822
+ return resolvedVoid;
1823
+ }
1824
+ catch (error) {
1825
+ return Promise.reject(error);
872
1826
  }
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
1827
  }
879
1828
  async rmdir(path, options = {}) {
880
1829
  options.signal?.throwIfAborted();
@@ -897,18 +1846,38 @@ export class MemoryFileSystem {
897
1846
  this.releaseNode(node);
898
1847
  this.changed(location.parent);
899
1848
  }
900
- async rm(path, options = {}) {
901
- options.signal?.throwIfAborted();
902
- let location;
1849
+ rm(path, options = {}) {
903
1850
  try {
904
- location = this.entry(path, "rm");
1851
+ options.signal?.throwIfAborted();
1852
+ const bound = activeConditionalMutations.get(this.identityScope);
1853
+ const active = bound?.path === path ? bound : undefined;
1854
+ const ancestors = options.ancestors ?? active?.ancestors;
1855
+ const expectedParent = options.parent ?? active?.parent;
1856
+ const expectedNode = options.expected ?? active?.expected;
1857
+ if (ancestors !== undefined)
1858
+ this.verifyDirectoryAncestry(ancestors, options);
1859
+ let location;
1860
+ try {
1861
+ location = this.entry(path, "rm", expectedNode !== undefined || expectedParent !== undefined);
1862
+ }
1863
+ catch (error) {
1864
+ if (options.force && error instanceof FsError && error.code === "ENOENT")
1865
+ return resolvedVoid;
1866
+ throw error;
1867
+ }
1868
+ if (expectedParent !== undefined)
1869
+ this.expectEntry(location.parent, expectedParent, path, false);
1870
+ if (expectedNode !== undefined) {
1871
+ if (options.force && !location.node)
1872
+ return resolvedVoid;
1873
+ this.expectEntry(location.node, expectedNode, path, location.node?.type !== "directory");
1874
+ }
1875
+ this.removeLocation(location, path, "rm", options.recursive === true);
1876
+ return resolvedVoid;
905
1877
  }
906
1878
  catch (error) {
907
- if (options.force && error instanceof FsError && error.code === "ENOENT")
908
- return;
909
- throw error;
1879
+ return Promise.reject(error);
910
1880
  }
911
- this.removeLocation(location, path, "rm", options.recursive === true);
912
1881
  }
913
1882
  removeLocation(location, path, syscall, recursive) {
914
1883
  const node = location.node;
@@ -919,28 +1888,40 @@ export class MemoryFileSystem {
919
1888
  this.permission(location.parent, 3, syscall, path);
920
1889
  if (node.type === "directory" && !recursive)
921
1890
  this.fail("EISDIR", syscall, path);
922
- const removed = [node];
1891
+ const cache = memoryCaches.get(this.ledger);
1892
+ cache.clearWrites();
1893
+ const removed = cache.removedScratch;
1894
+ removed.length = 0;
1895
+ removed.push(node);
1896
+ let keyChars = 0;
1897
+ let keyEntries = 0;
923
1898
  for (let index = 0; index < removed.length; index++) {
924
1899
  const entry = removed[index];
925
1900
  if (entry.type === "directory" && entry.entries.size > 0) {
926
1901
  this.permission(entry, 7, syscall, path);
927
- for (const child of entry.entries.values())
1902
+ for (const [name, child] of entry.entries) {
928
1903
  removed.push(child);
1904
+ keyChars += name.length;
1905
+ keyEntries++;
1906
+ }
929
1907
  }
930
1908
  }
931
1909
  location.parent.entries.delete(location.name);
932
- this.ledger.release(location.name.length * 2, 1);
933
- for (const entry of removed) {
1910
+ this.ledger.release((location.name.length + keyChars) * 2, 1 + keyEntries);
1911
+ const now = Date.now();
1912
+ for (let i = 0; i < removed.length; i++) {
1913
+ const entry = removed[i];
934
1914
  if (entry.type === "directory") {
935
- for (const name of entry.entries.keys())
936
- this.ledger.release(name.length * 2, 1);
937
1915
  entry.entries.clear();
938
1916
  }
939
1917
  entry.nlink--;
940
- entry.ctimeMs = Date.now();
941
- entry.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, entry.revision + 1);
1918
+ if (entry.nlink !== 0 || entry.references !== 0) {
1919
+ entry.ctimeMs = now;
1920
+ entry.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, entry.revision + 1);
1921
+ }
942
1922
  this.releaseNode(entry);
943
1923
  }
1924
+ removed.length = 0;
944
1925
  this.changed(location.parent);
945
1926
  }
946
1927
  unlink(path, options = {}) {
@@ -984,6 +1965,7 @@ export class MemoryFileSystem {
984
1965
  this.ledger.release(Math.max(0, nameGrowth), 0);
985
1966
  throw error;
986
1967
  }
1968
+ memoryCaches.get(this.ledger).clearWrites();
987
1969
  origin.parent.entries.delete(origin.name);
988
1970
  if (target.node) {
989
1971
  this.ledger.release(origin.name.length * 2, 1);
@@ -1029,11 +2011,17 @@ export class MemoryFileSystem {
1029
2011
  options.signal?.throwIfAborted();
1030
2012
  return this.resolve(path, "realpath").path;
1031
2013
  }
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);
2014
+ access(path, mode = 0, options = {}) {
2015
+ try {
2016
+ options.signal?.throwIfAborted();
2017
+ if (!Number.isInteger(mode) || mode < 0 || mode > 7)
2018
+ this.fail("EINVAL", "access", path);
2019
+ this.permission(this.resolveNode(path, "access", true), mode, "access", path);
2020
+ return resolvedVoid;
2021
+ }
2022
+ catch (error) {
2023
+ return Promise.reject(error);
2024
+ }
1037
2025
  }
1038
2026
  async readlink(path, options = {}) {
1039
2027
  options.signal?.throwIfAborted();
@@ -1078,7 +2066,28 @@ export class MemoryFileSystem {
1078
2066
  async chmod(path, mode, options = {}) {
1079
2067
  options.signal?.throwIfAborted();
1080
2068
  const permissions = this.mode(mode, 0, "chmod", path);
1081
- const node = this.resolve(path, "chmod").node;
2069
+ const bound = activeConditionalMutations.get(this.identityScope);
2070
+ const active = bound?.path === path ? bound : undefined;
2071
+ const conditional = snapshotConditionalChmod(path, active ? { ...active, ...options } : options);
2072
+ const controls = conditional ?? options;
2073
+ const ancestors = conditional?.ancestors;
2074
+ const expectedParent = conditional?.parent;
2075
+ const expectedNode = conditional?.expected;
2076
+ if (conditional?.commitGuard)
2077
+ runStagingGuard(conditional.commitGuard);
2078
+ controls.signal?.throwIfAborted();
2079
+ if (ancestors !== undefined)
2080
+ this.verifyDirectoryAncestry(ancestors, controls);
2081
+ const location = expectedNode !== undefined || expectedParent !== undefined
2082
+ ? this.entry(path, "chmod", true)
2083
+ : this.resolve(path, "chmod");
2084
+ if (expectedParent !== undefined)
2085
+ this.expectEntry(location.parent, expectedParent, path, false);
2086
+ if (expectedNode !== undefined)
2087
+ this.expectEntry(location.node, expectedNode, path, location.node?.type !== "directory");
2088
+ const node = location.node;
2089
+ if (node.type === "directory")
2090
+ memoryCaches.get(this.ledger).clearWrites();
1082
2091
  node.mode = typeModes[node.type] | permissions;
1083
2092
  node.ctimeMs = Date.now();
1084
2093
  node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
@@ -1239,39 +2248,8 @@ export class MemoryFileSystem {
1239
2248
  },
1240
2249
  };
1241
2250
  }
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
- }
2251
+ readStream(path, options = {}) {
2252
+ return new MemoryReadStream(this, path, options);
1275
2253
  }
1276
2254
  async writeStream(path, source, options = {}) {
1277
2255
  options.signal?.throwIfAborted();
@@ -1280,7 +2258,9 @@ export class MemoryFileSystem {
1280
2258
  let node;
1281
2259
  try {
1282
2260
  node = this.openWrite(path, options, "writeStream");
2261
+ const guard = options[extractionStreamGuard];
1283
2262
  node.references++;
2263
+ guard?.(node);
1284
2264
  const append = options.flag === "a" || options.flag === "ax";
1285
2265
  let position = 0;
1286
2266
  if (!append) {
@@ -1289,6 +2269,7 @@ export class MemoryFileSystem {
1289
2269
  }
1290
2270
  for await (const chunk of source) {
1291
2271
  options.signal?.throwIfAborted();
2272
+ guard?.(node);
1292
2273
  if (!(chunk instanceof Uint8Array))
1293
2274
  throw new TypeError("Memory files require Uint8Array data");
1294
2275
  this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
@@ -1306,37 +2287,407 @@ export class MemoryFileSystem {
1306
2287
  }
1307
2288
  }
1308
2289
  const memoryImplementation = Object.getOwnPropertyDescriptors(MemoryFileSystem.prototype);
2290
+ const memoryImplementationKeys = Object.keys(memoryImplementation);
2291
+ const memoryImplementationDescriptors = memoryImplementationKeys.map(key => memoryImplementation[key]);
2292
+ const MEMORY_DESCRIPTOR_CAPABILITIES = Object.freeze({
2293
+ noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
2294
+ });
2295
+ const memoryDescriptorGetPosition = async (retained) => retained.position;
2296
+ const memoryDescriptorStat = async (retained) => retained.fs.snapshot(retained.node);
2297
+ const memoryDescriptorRead = async (retained, buffer, position) => {
2298
+ const inode = retained.node;
2299
+ const start = position ?? retained.position;
2300
+ const count = Math.min(buffer.byteLength, Math.max(0, inode.data.byteLength - start));
2301
+ buffer.set(inode.data.subarray(start, start + count));
2302
+ if (position === null)
2303
+ retained.position += count;
2304
+ inode.atimeMs = Date.now();
2305
+ return count;
2306
+ };
2307
+ const memoryDescriptorWrite = async (retained, buffer, position) => {
2308
+ const inode = retained.node;
2309
+ const start = retained.append ? inode.data.byteLength : position ?? retained.position;
2310
+ const end = start + buffer.byteLength;
2311
+ retained.fs.writeAt(inode, buffer, start, "write", retained.path);
2312
+ if (position === null)
2313
+ retained.position = end;
2314
+ return buffer.byteLength;
2315
+ };
2316
+ const memoryDescriptorTruncate = async (retained, length) => {
2317
+ retained.fs.resizeNode(retained.node, length, "ftruncate", retained.path);
2318
+ };
2319
+ const memoryDescriptorSync = async () => { };
2320
+ const memoryDescriptorClose = async (retained) => {
2321
+ const inode = retained.node;
2322
+ retained.node = undefined;
2323
+ retained.fs.releaseReference(inode, retained.path);
2324
+ };
2325
+ const stockDescriptorWriteMethodNames = [
2326
+ "writeStream", "writeFile", "appendFile", "access", "stat", "lstat", "realpath",
2327
+ "openWrite", "prepareWrite", "writeData", "addNode", "replaceData", "releaseReference", "releaseNode",
2328
+ "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "admitSize", "changed", "metadata", "integer", "writeAt", "fail",
2329
+ ];
2330
+ const stockRetainedResizeMethodNames = [
2331
+ "openResizeFile", "truncate", "resizeNode", "snapshot",
2332
+ ];
1309
2333
  function stockDescriptorWrite(filesystem) {
1310
- if (Object.getPrototypeOf(filesystem) !== MemoryFileSystem.prototype)
2334
+ if (!isStockMemoryMethods(filesystem, stockDescriptorWriteMethodNames, true))
1311
2335
  return false;
1312
2336
  const owner = ownedStores.get(filesystem);
1313
- if (!owner || Object.getOwnPropertyDescriptor(filesystem, "root")?.value !== owner.root
1314
- || Object.getOwnPropertyDescriptor(filesystem, "capabilities")?.value !== owner.capabilities)
2337
+ const capDesc = Object.getOwnPropertyDescriptor(filesystem, "capabilities");
2338
+ return !!capDesc && "value" in capDesc && capDesc.value === owner.capabilities;
2339
+ }
2340
+ function stockRetainedResize(filesystem) {
2341
+ if (!stockDescriptorWrite(filesystem) || !isStockMemoryMethods(filesystem, stockRetainedResizeMethodNames, false))
1315
2342
  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)
2343
+ const owner = ownedStores.get(filesystem);
2344
+ const ledgerDesc = Object.getOwnPropertyDescriptor(filesystem, "ledger");
2345
+ return !!ledgerDesc && "value" in ledgerDesc && ledgerDesc.value === owner.ledger;
2346
+ }
2347
+ export function createMemoryFileSystem(options = {}) {
2348
+ return new MemoryFileSystem(options);
2349
+ }
2350
+ const missingTargetMethodNames = ["realpath", "lstat", "resolve", "permission", "validatePath", "fail", "snapshot"];
2351
+ const deviceFastMethodNames = ["lstat", "readlink", "resolve", "permission", "validatePath", "fail", "snapshot"];
2352
+ const readFileFastMethodNames = ["readFile", "file", "resolve", "permission", "validatePath", "fail", "integer"];
2353
+ const writeFileFastMethodNames = [
2354
+ "writeFile", "appendFile", "writeData", "prepareWrite", "openWrite", "writeAt",
2355
+ "addNode", "replaceData", "resolve", "permission", "validatePath", "mode",
2356
+ "bytes", "allocate", "admitSize", "changed", "metadata", "fail",
2357
+ ];
2358
+ const mkdirFastMethodNames = [
2359
+ "mkdir", "lstat", "stat", "resolve", "permission", "validatePath", "mode",
2360
+ "addNode", "directory", "changed", "fail",
2361
+ ];
2362
+ const rmFastMethodNames = [
2363
+ "rm", "lstat", "stat", "readdir", "entry", "resolve", "removeLocation",
2364
+ "permission", "validatePath", "terminalDot", "releaseNode", "changed", "fail",
2365
+ ];
2366
+ const openRedirectFastMethodNames = [
2367
+ "open", "writeFile", "appendFile", "rename", "writeData", "prepareWrite", "openWrite",
2368
+ "resizeNode", "writeAt", "addNode", "replaceData", "resolve", "permission",
2369
+ "validatePath", "mode", "bytes", "allocate", "admitSize", "changed", "metadata",
2370
+ "releaseReference", "releaseNode", "fail",
2371
+ ];
2372
+ export class MemoryRedirectHandle {
2373
+ constructor(fs, path, node, append) {
2374
+ this.fs = fs;
2375
+ this.path = path;
2376
+ this.node = node;
2377
+ this.append = append;
2378
+ this.position = 0;
2379
+ this.closed = false;
2380
+ }
2381
+ writeSync(chunk, signal) {
2382
+ signal?.throwIfAborted();
2383
+ if (this.closed)
2384
+ throw new FsError("EBADF", { syscall: "write", path: this.path });
2385
+ if (chunk.byteLength === 0)
2386
+ return;
2387
+ const pos = this.append ? this.node.byteLength : this.position;
2388
+ this.fs.writeAt(this.node, chunk, pos, "write", this.path);
2389
+ if (!this.append)
2390
+ this.position = pos + chunk.byteLength;
2391
+ }
2392
+ close() {
2393
+ if (this.closed)
2394
+ return;
2395
+ this.closed = true;
2396
+ this.fs.releaseReference(this.node, this.path);
2397
+ }
2398
+ }
2399
+ export function tryOpenMemoryRedirectHandleSync(filesystem, path, append, mode, signal) {
2400
+ const mem = filesystem;
2401
+ const owner = ownedStores.get(mem);
2402
+ if (!owner ||
2403
+ mem.symlinkCount !== 0 ||
2404
+ mem.capabilities !== owner.capabilities ||
2405
+ activeConditionalMutations.has(mem.identityScope) ||
2406
+ !isStockMemoryMethods(mem, openRedirectFastMethodNames, false) ||
2407
+ !isCleanAbsolutePath(path) ||
2408
+ path === "/dev" ||
2409
+ path.startsWith("/dev/")) {
2410
+ return undefined;
2411
+ }
2412
+ signal?.throwIfAborted();
2413
+ const validMode = mem.mode(mode, 0o666, "open", path);
2414
+ const location = mem.resolve(path, "open", {
2415
+ followFinal: true,
2416
+ allowMissing: true,
2417
+ });
2418
+ let node = location.node;
2419
+ if (node) {
2420
+ if (node.type !== "file")
2421
+ mem.fail("EISDIR", "open", path);
2422
+ mem.permission(node, 2, "open", path);
2423
+ }
2424
+ else {
2425
+ mem.permission(location.parent, 3, "open", path);
2426
+ }
2427
+ const ledger = mem.ledger;
2428
+ ledger.reserve(path.length * 2, 1, "open", path);
2429
+ try {
2430
+ node ??= mem.openWrite(path, {}, "open", { location, mode: validMode, append: false });
2431
+ if (!append) {
2432
+ mem.resizeNode(node, 0, "open", path);
2433
+ }
2434
+ }
2435
+ catch (error) {
2436
+ ledger.release(path.length * 2, 1);
2437
+ throw error;
2438
+ }
2439
+ node.references++;
2440
+ return new MemoryRedirectHandle(mem, path, node, append);
2441
+ }
2442
+ export function utf8ByteLength(value) {
2443
+ let bytes = 0;
2444
+ for (let i = 0; i < value.length; i++) {
2445
+ const code = value.charCodeAt(i);
2446
+ if (code < 0x80)
2447
+ bytes += 1;
2448
+ else if (code < 0x800)
2449
+ bytes += 2;
2450
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
2451
+ bytes += 4;
2452
+ i++;
2453
+ }
2454
+ else
2455
+ bytes += 3;
2456
+ }
2457
+ return bytes;
2458
+ }
2459
+ function exceedsComponentByteLimit(value) {
2460
+ return value.length > 85 && (value.length > 255 || utf8ByteLength(value) > 255);
2461
+ }
2462
+ export function isCleanAbsolutePath(path) {
2463
+ const len = path.length;
2464
+ if (len <= 1 || len > 65536 || path.charCodeAt(0) !== 47 || path.charCodeAt(len - 1) === 47)
2465
+ return false;
2466
+ let segStart = 1;
2467
+ let components = 1;
2468
+ for (let i = 1; i < len; i++) {
2469
+ const c = path.charCodeAt(i);
2470
+ if (c === 0)
1322
2471
  return false;
2472
+ if (c === 47) {
2473
+ const segLen = i - segStart;
2474
+ if (segLen === 0)
2475
+ return false;
2476
+ if (segLen === 1 && path.charCodeAt(segStart) === 46)
2477
+ return false;
2478
+ if (segLen === 2 && path.charCodeAt(segStart) === 46 && path.charCodeAt(segStart + 1) === 46)
2479
+ return false;
2480
+ if (++components > 256)
2481
+ return false;
2482
+ segStart = i + 1;
2483
+ }
1323
2484
  }
2485
+ const lastLen = len - segStart;
2486
+ if (lastLen === 1 && path.charCodeAt(segStart) === 46)
2487
+ return false;
2488
+ if (lastLen === 2 && path.charCodeAt(segStart) === 46 && path.charCodeAt(segStart + 1) === 46)
2489
+ return false;
1324
2490
  return true;
1325
2491
  }
1326
- function stockRetainedResize(filesystem) {
1327
- if (!stockDescriptorWrite(filesystem))
2492
+ export function tryResolveMemoryDevicePath(filesystem, path, resizeCreate) {
2493
+ const mem = filesystem;
2494
+ if (!ownedStores.has(mem) || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, deviceFastMethodNames, false))
2495
+ return undefined;
2496
+ if (!isCleanAbsolutePath(path) || path === "/dev" || path.startsWith("/dev/"))
2497
+ return undefined;
2498
+ let current = mem.root;
2499
+ let start = 1;
2500
+ while (true) {
2501
+ if (((current.mode >> 6) & 1) !== 1)
2502
+ return undefined;
2503
+ const slash = path.indexOf("/", start);
2504
+ if (slash === -1) {
2505
+ const name = path.slice(start);
2506
+ if (exceedsComponentByteLimit(name))
2507
+ return undefined;
2508
+ if (resizeCreate === false && !current.entries.has(name))
2509
+ return undefined;
2510
+ return path;
2511
+ }
2512
+ const name = path.slice(start, slash);
2513
+ if (exceedsComponentByteLimit(name))
2514
+ return undefined;
2515
+ const next = current.entries.get(name);
2516
+ if (!next) {
2517
+ if (resizeCreate !== undefined)
2518
+ return undefined;
2519
+ let remStart = slash + 1;
2520
+ while (true) {
2521
+ const nextSlash = path.indexOf("/", remStart);
2522
+ const seg = nextSlash === -1 ? path.slice(remStart) : path.slice(remStart, nextSlash);
2523
+ if (exceedsComponentByteLimit(seg))
2524
+ return undefined;
2525
+ if (nextSlash === -1)
2526
+ return path;
2527
+ remStart = nextSlash + 1;
2528
+ }
2529
+ }
2530
+ if (next.type !== "directory")
2531
+ return undefined;
2532
+ current = next;
2533
+ start = slash + 1;
2534
+ }
2535
+ }
2536
+ function isStockMemoryMethods(mem, names, checkRootAccessor = true) {
2537
+ const owner = ownedStores.get(mem);
2538
+ if (!owner || Object.getPrototypeOf(mem) !== MemoryFileSystem.prototype)
1328
2539
  return false;
1329
- const owner = ownedStores.get(filesystem);
1330
- if (!owner || Object.getOwnPropertyDescriptor(filesystem, "ledger")?.value !== owner.ledger)
2540
+ if (checkRootAccessor) {
2541
+ const rootDesc = Object.getOwnPropertyDescriptor(mem, "root");
2542
+ if (!rootDesc || !("value" in rootDesc) || rootDesc.value !== owner.root)
2543
+ return false;
2544
+ }
2545
+ else if (mem.root !== owner.root) {
1331
2546
  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)
2547
+ }
2548
+ for (let i = 0; i < names.length; i++) {
2549
+ const name = names[i];
2550
+ if (Object.prototype.hasOwnProperty.call(mem, name)) {
2551
+ const descriptor = Object.getOwnPropertyDescriptor(mem, name);
2552
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
2553
+ return false;
2554
+ }
2555
+ else if (MemoryFileSystem.prototype[name] !== memoryImplementation[name]?.value) {
1336
2556
  return false;
2557
+ }
1337
2558
  }
1338
2559
  return true;
1339
2560
  }
1340
- export function createMemoryFileSystem(options = {}) {
1341
- return new MemoryFileSystem(options);
2561
+ export function tryReadMemoryFileViewSync(filesystem, path, maxBytes, signal) {
2562
+ const mem = filesystem;
2563
+ if (!ownedStores.has(mem) || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
2564
+ return undefined;
2565
+ signal?.throwIfAborted();
2566
+ if (maxBytes !== undefined)
2567
+ mem.integer(maxBytes, "readFile", path);
2568
+ const node = mem.file(path, "readFile");
2569
+ mem.permission(node, 4, "readFile", path);
2570
+ if (maxBytes !== undefined && node.data.byteLength > maxBytes)
2571
+ mem.fail("EFBIG", "readFile", path);
2572
+ node.atimeMs = Date.now();
2573
+ return node.data;
2574
+ }
2575
+ export function tryGetMemoryDirectoryEntryNamesSync(filesystem, path) {
2576
+ const mem = filesystem;
2577
+ if (!ownedStores.has(mem) || mem.symlinkCount !== 0 || !isStockMemoryMethods(mem, readFileFastMethodNames, false))
2578
+ return undefined;
2579
+ const root = mem.root;
2580
+ if (path === "/") {
2581
+ if (((root.mode >> 6) & 4) !== 4)
2582
+ return undefined;
2583
+ return root.entries;
2584
+ }
2585
+ if (!isCleanAbsolutePath(path) || path === "/dev" || path.startsWith("/dev/"))
2586
+ return undefined;
2587
+ let current = root;
2588
+ let start = 1;
2589
+ while (true) {
2590
+ if (((current.mode >> 6) & 1) !== 1)
2591
+ return undefined;
2592
+ const slash = path.indexOf("/", start);
2593
+ if (slash === -1) {
2594
+ const next = current.entries.get(path.slice(start));
2595
+ if (!next || next.type !== "directory" || ((next.mode >> 6) & 4) !== 4)
2596
+ return undefined;
2597
+ return next.entries;
2598
+ }
2599
+ const next = current.entries.get(path.slice(start, slash));
2600
+ if (!next || next.type !== "directory")
2601
+ return undefined;
2602
+ current = next;
2603
+ start = slash + 1;
2604
+ }
2605
+ }
2606
+ export function tryWriteMemoryFileSync(filesystem, path, data, append, mode, signal) {
2607
+ const mem = filesystem;
2608
+ const owner = ownedStores.get(mem);
2609
+ if (!owner ||
2610
+ mem.symlinkCount !== 0 ||
2611
+ mem.capabilities !== owner.capabilities ||
2612
+ !isStockMemoryMethods(mem, writeFileFastMethodNames, false) ||
2613
+ !isCleanAbsolutePath(path) ||
2614
+ path === "/dev" ||
2615
+ path.startsWith("/dev/")) {
2616
+ return false;
2617
+ }
2618
+ signal?.throwIfAborted();
2619
+ mem.writeMemoryFileFast(path, data, append, mode);
2620
+ return true;
2621
+ }
2622
+ export function tryMkdirMemorySync(filesystem, path, recursive, mode, signal) {
2623
+ const mem = filesystem;
2624
+ const owner = ownedStores.get(mem);
2625
+ if (!owner ||
2626
+ mem.symlinkCount !== 0 ||
2627
+ mem.capabilities !== owner.capabilities ||
2628
+ !isStockMemoryMethods(mem, mkdirFastMethodNames, false) ||
2629
+ !isCleanAbsolutePath(path) ||
2630
+ path === "/dev" ||
2631
+ path.startsWith("/dev/")) {
2632
+ return false;
2633
+ }
2634
+ signal?.throwIfAborted();
2635
+ const validMode = mem.mode(mode, 0o777, "mkdir", path);
2636
+ if (recursive) {
2637
+ const node = mem.resolve(path, "mkdir", { createDirectories: validMode }).node;
2638
+ if (node.type !== "directory")
2639
+ mem.fail("EEXIST", "mkdir", path);
2640
+ return true;
2641
+ }
2642
+ const location = mem.resolve(path, "mkdir", { allowMissing: true, followFinal: false });
2643
+ if (location.node)
2644
+ mem.fail("EEXIST", "mkdir", path);
2645
+ mem.permission(location.parent, 3, "mkdir", path);
2646
+ mem.addDirectoryNode(location.parent, location.name, validMode, "mkdir", path);
2647
+ return true;
2648
+ }
2649
+ export function tryRmRfMemorySync(filesystem, path, signal) {
2650
+ const mem = filesystem;
2651
+ const owner = ownedStores.get(mem);
2652
+ if (!owner ||
2653
+ mem.symlinkCount !== 0 ||
2654
+ mem.capabilities !== owner.capabilities ||
2655
+ activeConditionalMutations.has(mem.identityScope) ||
2656
+ !isStockMemoryMethods(mem, rmFastMethodNames, false) ||
2657
+ !isCleanAbsolutePath(path) ||
2658
+ path === "/dev" ||
2659
+ path.startsWith("/dev/")) {
2660
+ return false;
2661
+ }
2662
+ signal?.throwIfAborted();
2663
+ let location;
2664
+ try {
2665
+ location = mem.resolve(path, "rm", noFollowResolveOptions);
2666
+ }
2667
+ catch (error) {
2668
+ if (error instanceof FsError && error.code === "ENOENT")
2669
+ return true;
2670
+ throw error;
2671
+ }
2672
+ mem.removeLocation(location, path, "rm", true);
2673
+ return true;
2674
+ }
2675
+ export function tryWriteMemoryFileInDirSync(filesystem, dirPrefix, name, data, append, mode, signal) {
2676
+ const mem = filesystem;
2677
+ const owner = ownedStores.get(mem);
2678
+ if (!owner ||
2679
+ mem.symlinkCount !== 0 ||
2680
+ mem.capabilities !== owner.capabilities ||
2681
+ !isStockMemoryMethods(mem, writeFileFastMethodNames, false) ||
2682
+ name.length === 0 ||
2683
+ name === "." ||
2684
+ name === ".." ||
2685
+ name.indexOf("/") !== -1 ||
2686
+ name.indexOf("\0") !== -1 ||
2687
+ dirPrefix.length + name.length > 65536) {
2688
+ return false;
2689
+ }
2690
+ signal?.throwIfAborted();
2691
+ mem.writeMemoryFileInDirFast(dirPrefix, name, data, append, mode);
2692
+ return true;
1342
2693
  }