@poe-platform/safe-fs 0.1.728 → 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.
@@ -15,19 +15,6 @@ import { MemoryAllocation, MemoryLedger } from "./ledger.js";
15
15
  import { normalizeMemoryFileSystemLimits } from "./limits.js";
16
16
  export { defaultMemoryFileSystemLimits } from "./limits.js";
17
17
  class MemoryFileNode {
18
- type = "file";
19
- revision = 0;
20
- mode;
21
- ino;
22
- nlink = 1;
23
- atimeMs;
24
- mtimeMs;
25
- ctimeMs;
26
- birthtimeMs;
27
- references = 0;
28
- byteLength;
29
- view;
30
- allocation;
31
18
  constructor(mode, ino, now, byteLength, allocation, view) {
32
19
  this.mode = mode;
33
20
  this.ino = ino;
@@ -47,17 +34,50 @@ class MemoryFileNode {
47
34
  this.byteLength = value.byteLength;
48
35
  }
49
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
+ });
50
66
  class MemoryCache {
51
67
  allocations = [];
52
68
  files = [];
69
+ directories = [];
70
+ removedScratch = [];
53
71
  lastFastDirPrefix = "";
54
72
  lastFastDirNode;
55
73
  lastFastFilePath = "";
74
+ lastFastFileName = "";
56
75
  lastFastFileNode;
57
76
  clearWrites() {
58
77
  this.lastFastDirPrefix = "";
59
78
  this.lastFastDirNode = undefined;
60
79
  this.lastFastFilePath = "";
80
+ this.lastFastFileName = "";
61
81
  this.lastFastFileNode = undefined;
62
82
  }
63
83
  }
@@ -70,6 +90,14 @@ const ext4HtreeEof64 = (1n << 63n) - 1n;
70
90
  const extractionStreamGuard = Symbol("extractionStreamGuard");
71
91
  const ownedStats = new WeakMap();
72
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
+ }
73
101
  // Forwarded receivers share the ledger, so mutations invalidate the owner's cache.
74
102
  const memoryCaches = new WeakMap();
75
103
  const registeredAuthorities = new WeakSet();
@@ -127,6 +155,107 @@ export async function bindConditionalMutation(identityScope, binding, action) {
127
155
  }
128
156
  }
129
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
+ }
130
259
  export class MemoryFileSystem {
131
260
  capabilitiesFor;
132
261
  capabilities = ((filesystem) => {
@@ -250,8 +379,8 @@ export class MemoryFileSystem {
250
379
  }
251
380
  retained.set("/", this.root);
252
381
  }
253
- const capabilities = Object.freeze({ ...this.capabilities, retainedStagingCleanup: false });
254
- 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"]);
255
384
  const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
256
385
  const check = (path, followFinal) => {
257
386
  if (!roots.some(root => root === "/" || path === root || path.startsWith(`${root}/`)))
@@ -287,7 +416,7 @@ export class MemoryFileSystem {
287
416
  throw new FsError("ENOTSUP", { syscall: String(property) });
288
417
  // Stock mutations resolve and commit synchronously. Streamed writes
289
418
  // repeat the boundary and file-binding check at each backend mutation.
290
- const paths = property === "symlink" ? [args[1]] : property === "link" || property === "rename" ? args.slice(0, 2) : [args[0]];
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]];
291
420
  for (const path of paths) {
292
421
  if (typeof path !== "string")
293
422
  throw new FsError("EINVAL");
@@ -314,7 +443,39 @@ export class MemoryFileSystem {
314
443
  };
315
444
  }
316
445
  directory(mode) {
317
- 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
+ }
318
479
  }
319
480
  addNode(parent, name, create, syscall, path, retainedBytes = 0) {
320
481
  const bytes = name.length * 2 + retainedBytes;
@@ -344,6 +505,7 @@ export class MemoryFileSystem {
344
505
  if (node.type === "file") {
345
506
  if (cache.lastFastFileNode === node) {
346
507
  cache.lastFastFilePath = "";
508
+ cache.lastFastFileName = "";
347
509
  cache.lastFastFileNode = undefined;
348
510
  }
349
511
  this.totalBytes -= node.byteLength;
@@ -360,9 +522,14 @@ export class MemoryFileSystem {
360
522
  else if (node.type === "symlink") {
361
523
  this.symlinkCount--;
362
524
  }
363
- else if (cache.lastFastDirNode === node) {
364
- cache.lastFastDirPrefix = "";
365
- cache.lastFastDirNode = undefined;
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
+ }
366
533
  }
367
534
  }
368
535
  releaseReference(node, path) {
@@ -408,9 +575,9 @@ export class MemoryFileSystem {
408
575
  if (!Number.isSafeInteger(value) || value < 0)
409
576
  this.fail("EINVAL", syscall, path);
410
577
  }
411
- changed(node) {
578
+ changed(node, now = Date.now()) {
412
579
  node.revision = node.revision < 1073741823 ? (node.revision + 1) | 0 : Math.min(Number.MAX_SAFE_INTEGER + 1, node.revision + 1);
413
- node.mtimeMs = node.ctimeMs = Date.now();
580
+ node.mtimeMs = node.ctimeMs = now;
414
581
  }
415
582
  resolveNode(path, syscall, followFinal = true) {
416
583
  const cache = memoryCaches.get(this.ledger);
@@ -462,7 +629,7 @@ export class MemoryFileSystem {
462
629
  }
463
630
  resolve(path, syscall, options = emptyResolveOptions) {
464
631
  this.validatePath(path, syscall);
465
- if (this.symlinkCount === 0 && options.createDirectories === undefined && options.resizeCreate === undefined
632
+ if (this.symlinkCount === 0 && options.resizeCreate === undefined
466
633
  && options.resolutionSteps === undefined && isCleanAbsolutePath(path)) {
467
634
  let current = this.root;
468
635
  let start = 1;
@@ -473,7 +640,11 @@ export class MemoryFileSystem {
473
640
  const name = path.slice(start);
474
641
  if (exceedsComponentByteLimit(name))
475
642
  this.fail("ENAMETOOLONG", syscall, path);
476
- const node = current.entries.get(name);
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
+ }
477
648
  if (!node && !options.allowMissing)
478
649
  this.fail("ENOENT", syscall, path);
479
650
  return { node, parent: current, name, path };
@@ -481,7 +652,11 @@ export class MemoryFileSystem {
481
652
  const name = path.slice(start, slash);
482
653
  if (exceedsComponentByteLimit(name))
483
654
  this.fail("ENAMETOOLONG", syscall, path);
484
- const next = current.entries.get(name);
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
+ }
485
660
  if (!next)
486
661
  this.fail("ENOENT", syscall, path);
487
662
  if (next.type !== "directory")
@@ -645,6 +820,12 @@ export class MemoryFileSystem {
645
820
  return location;
646
821
  }
647
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;
648
829
  return /(?:^|\/)\.{1,2}\/*$/.test(path);
649
830
  }
650
831
  directoryNlink(node) {
@@ -716,6 +897,9 @@ export class MemoryFileSystem {
716
897
  allocate(length, syscall, path) {
717
898
  this.ledger.fileSize(length, syscall, path);
718
899
  this.ledger.reserve(length, 0, syscall, path);
900
+ if (length === 0) {
901
+ return new MemoryAllocation(EMPTY_ALLOC_BYTES, this.ledger);
902
+ }
719
903
  const allocations = memoryCaches.get(this.ledger).allocations;
720
904
  if (length === 64 && allocations.length > 0) {
721
905
  const pooled = allocations.pop();
@@ -723,6 +907,17 @@ export class MemoryFileSystem {
723
907
  return pooled;
724
908
  }
725
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
+ }
726
921
  return new MemoryAllocation(new Uint8Array(length), this.ledger);
727
922
  }
728
923
  catch (cause) {
@@ -873,7 +1068,13 @@ export class MemoryFileSystem {
873
1068
  let allocation = node.allocation;
874
1069
  if (length > allocation.data.byteLength) {
875
1070
  this.ledger.check(length, 0, syscall, path);
876
- const capacity = Math.min(Math.max(length, curLen * 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);
877
1078
  allocation = this.allocate(capacity, syscall, path);
878
1079
  }
879
1080
  try {
@@ -977,13 +1178,12 @@ export class MemoryFileSystem {
977
1178
  if (current && append) {
978
1179
  this.writeAt(current, data, current.byteLength, syscall, path);
979
1180
  cache.lastFastFilePath = path;
1181
+ cache.lastFastFileName = name;
980
1182
  cache.lastFastFileNode = current;
981
1183
  return;
982
1184
  }
983
1185
  if (!current) {
984
- const capacity = length > 0 && (append || (length < 64 && this.ledger.availableBytes > 65536 && this.ledger.limits.maxFileBytes >= 64))
985
- ? Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - nameBytes)
986
- : length;
1186
+ const capacity = length;
987
1187
  const allocation = this.allocate(capacity, syscall, path);
988
1188
  try {
989
1189
  allocation.data.set(data);
@@ -991,7 +1191,7 @@ export class MemoryFileSystem {
991
1191
  try {
992
1192
  const now = Date.now();
993
1193
  const fileMode = typeModes.file | mode;
994
- const view = capacity === length ? allocation.data : undefined;
1194
+ const view = allocation.data;
995
1195
  let node = cache.files.pop();
996
1196
  if (node) {
997
1197
  node.mode = fileMode;
@@ -1010,16 +1210,16 @@ export class MemoryFileSystem {
1010
1210
  else {
1011
1211
  node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view);
1012
1212
  }
1013
- this.admitSize(undefined, length, syscall, path);
1014
1213
  const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
1015
1214
  parent.entries.set(name, node);
1016
1215
  this.totalBytes += length;
1017
- this.changed(parent);
1216
+ this.changed(parent, now);
1018
1217
  if (prevNlink !== undefined) {
1019
1218
  parent.cachedNlink = prevNlink;
1020
1219
  parent.cachedNlinkRev = parent.revision;
1021
1220
  }
1022
1221
  cache.lastFastFilePath = path;
1222
+ cache.lastFastFileName = name;
1023
1223
  cache.lastFastFileNode = node;
1024
1224
  }
1025
1225
  catch (error) {
@@ -1037,12 +1237,169 @@ export class MemoryFileSystem {
1037
1237
  try {
1038
1238
  this.replaceData(current, copied);
1039
1239
  this.changed(current);
1240
+ cache.lastFastFilePath = path;
1241
+ cache.lastFastFileName = name;
1242
+ cache.lastFastFileNode = current;
1040
1243
  }
1041
1244
  catch (error) {
1042
1245
  copied.release();
1043
1246
  throw error;
1044
1247
  }
1045
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
+ }
1046
1403
  async readFile(path, options = {}) {
1047
1404
  options.signal?.throwIfAborted();
1048
1405
  if (options.maxBytes !== undefined)
@@ -1143,6 +1500,8 @@ export class MemoryFileSystem {
1143
1500
  location.parent.entries.set(location.name, directory);
1144
1501
  if (file.type === "file")
1145
1502
  this.totalBytes += file.data.byteLength;
1503
+ else
1504
+ this.symlinkCount++;
1146
1505
  }
1147
1506
  catch (error) {
1148
1507
  allocation?.release();
@@ -1529,32 +1888,40 @@ export class MemoryFileSystem {
1529
1888
  this.permission(location.parent, 3, syscall, path);
1530
1889
  if (node.type === "directory" && !recursive)
1531
1890
  this.fail("EISDIR", syscall, path);
1532
- memoryCaches.get(this.ledger).clearWrites();
1533
- 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);
1534
1896
  let keyChars = 0;
1535
1897
  let keyEntries = 0;
1536
1898
  for (let index = 0; index < removed.length; index++) {
1537
1899
  const entry = removed[index];
1538
1900
  if (entry.type === "directory" && entry.entries.size > 0) {
1539
1901
  this.permission(entry, 7, syscall, path);
1540
- entry.entries.forEach((child, name) => {
1902
+ for (const [name, child] of entry.entries) {
1541
1903
  removed.push(child);
1542
1904
  keyChars += name.length;
1543
1905
  keyEntries++;
1544
- });
1906
+ }
1545
1907
  }
1546
1908
  }
1547
1909
  location.parent.entries.delete(location.name);
1548
1910
  this.ledger.release((location.name.length + keyChars) * 2, 1 + keyEntries);
1549
- for (const entry of removed) {
1911
+ const now = Date.now();
1912
+ for (let i = 0; i < removed.length; i++) {
1913
+ const entry = removed[i];
1550
1914
  if (entry.type === "directory") {
1551
1915
  entry.entries.clear();
1552
1916
  }
1553
1917
  entry.nlink--;
1554
- entry.ctimeMs = Date.now();
1555
- 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
+ }
1556
1922
  this.releaseNode(entry);
1557
1923
  }
1924
+ removed.length = 0;
1558
1925
  this.changed(location.parent);
1559
1926
  }
1560
1927
  unlink(path, options = {}) {
@@ -1882,94 +2249,7 @@ export class MemoryFileSystem {
1882
2249
  };
1883
2250
  }
1884
2251
  readStream(path, options = {}) {
1885
- return {
1886
- [Symbol.asyncIterator]: () => {
1887
- let initialized = false;
1888
- let finished = false;
1889
- let offset = 0;
1890
- let end = 0;
1891
- let chunkSize = preferredIoBlockSize;
1892
- let node;
1893
- let allocation;
1894
- let data;
1895
- const release = () => {
1896
- if (finished)
1897
- return;
1898
- finished = true;
1899
- if (allocation) {
1900
- allocation.release();
1901
- allocation = undefined;
1902
- }
1903
- if (node) {
1904
- const releasedNode = node;
1905
- node = undefined;
1906
- this.releaseReference(releasedNode, path);
1907
- }
1908
- data = undefined;
1909
- };
1910
- const step = () => {
1911
- if (finished)
1912
- return { done: true, value: undefined };
1913
- try {
1914
- options.signal?.throwIfAborted();
1915
- if (!initialized) {
1916
- initialized = true;
1917
- const start = options.start ?? 0;
1918
- chunkSize = options.chunkSize ?? preferredIoBlockSize;
1919
- this.integer(start, "readStream", path);
1920
- this.integer(chunkSize, "readStream", path);
1921
- if (chunkSize === 0)
1922
- this.fail("EINVAL", "readStream", path);
1923
- if (options.endExclusive !== undefined) {
1924
- this.integer(options.endExclusive, "readStream", path);
1925
- if (options.endExclusive < start)
1926
- this.fail("EINVAL", "readStream", path);
1927
- }
1928
- const target = this.file(path, "readStream");
1929
- this.permission(target, 4, "readStream", path);
1930
- this.ledger.reserve(path.length * 2, 1, "readStream", path);
1931
- target.references++;
1932
- node = target;
1933
- allocation = target.allocation;
1934
- allocation.retain();
1935
- data = target.data;
1936
- offset = start;
1937
- end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
1938
- target.atimeMs = Date.now();
1939
- }
1940
- if (offset < end) {
1941
- const nextEnd = Math.min(offset + chunkSize, end);
1942
- const chunk = data.slice(offset, nextEnd);
1943
- offset = nextEnd;
1944
- return { done: false, value: chunk };
1945
- }
1946
- release();
1947
- return { done: true, value: undefined };
1948
- }
1949
- catch (error) {
1950
- release();
1951
- throw error;
1952
- }
1953
- };
1954
- return {
1955
- abortSignal: options.signal,
1956
- [Symbol.asyncIterator]() { return this; },
1957
- tryNextSync: step,
1958
- next() {
1959
- try {
1960
- return Promise.resolve(step());
1961
- }
1962
- catch (error) {
1963
- return Promise.reject(error);
1964
- }
1965
- },
1966
- return() {
1967
- release();
1968
- return Promise.resolve({ done: true, value: undefined });
1969
- },
1970
- };
1971
- },
1972
- };
2252
+ return new MemoryReadStream(this, path, options);
1973
2253
  }
1974
2254
  async writeStream(path, source, options = {}) {
1975
2255
  options.signal?.throwIfAborted();
@@ -2075,6 +2355,90 @@ const writeFileFastMethodNames = [
2075
2355
  "addNode", "replaceData", "resolve", "permission", "validatePath", "mode",
2076
2356
  "bytes", "allocate", "admitSize", "changed", "metadata", "fail",
2077
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
+ }
2078
2442
  export function utf8ByteLength(value) {
2079
2443
  let bytes = 0;
2080
2444
  for (let i = 0; i < value.length; i++) {
@@ -2255,3 +2619,75 @@ export function tryWriteMemoryFileSync(filesystem, path, data, append, mode, sig
2255
2619
  mem.writeMemoryFileFast(path, data, append, mode);
2256
2620
  return true;
2257
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;
2693
+ }