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