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