@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,29 +1,37 @@
|
|
|
1
|
+
import { snapshotStagingCreation } from "../staging-cleanup.js";
|
|
1
2
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
2
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
4
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
4
5
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
|
|
5
6
|
import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
|
|
7
|
+
import { tryResolveMemoryDevicePath } from "../memory/index.js";
|
|
6
8
|
import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
|
|
7
9
|
import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
|
|
8
10
|
import { pathNamespace } from "../path-namespace.js";
|
|
11
|
+
import { runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "../staging-ancestry.js";
|
|
9
12
|
import { openFileDescriptor } from "../descriptor.js";
|
|
13
|
+
import { normalizePath } from "../../contracts/virtual-path.js";
|
|
10
14
|
const views = new WeakMap();
|
|
11
15
|
const deviceCapabilities = Object.freeze({
|
|
16
|
+
synchronousStagingResolution: false,
|
|
12
17
|
readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
|
|
13
18
|
write: true, append: true, exclusiveCreate: true, streamingRead: true, retainedRead: true,
|
|
14
19
|
streamingWrite: true, streamingAppend: true, independentWriteStreams: true, copy: true, exclusiveCopy: true,
|
|
15
20
|
remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
|
|
16
21
|
mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
|
|
17
22
|
permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
|
|
18
|
-
open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
23
|
+
open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
19
24
|
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
|
|
20
25
|
});
|
|
21
26
|
function globalCapabilities(filesystem) {
|
|
22
27
|
const capabilities = { readOnly: false };
|
|
23
28
|
const optional = {
|
|
24
29
|
open: ["open"],
|
|
30
|
+
retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
31
|
+
synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
32
|
+
synchronousStagingResolution: ["prepareStagingResolution"],
|
|
25
33
|
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
26
|
-
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
34
|
+
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
27
35
|
streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
|
|
28
36
|
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
29
37
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
|
|
@@ -36,6 +44,8 @@ function globalCapabilities(filesystem) {
|
|
|
36
44
|
if (key === "copy" || key === "exclusiveCopy")
|
|
37
45
|
return undefined;
|
|
38
46
|
let declared = filesystem.capabilities[key];
|
|
47
|
+
if (key === "atomicEntryRemovalReceipt")
|
|
48
|
+
declared = ownedMutationCapabilities(filesystem).atomicEntryRemovalReceipt;
|
|
39
49
|
if (declared === true && optional[key]?.some(method => typeof filesystem[method] !== "function"))
|
|
40
50
|
declared = false;
|
|
41
51
|
if (key === "retainedResize" && filesystem.capabilities.readOnly === true)
|
|
@@ -94,8 +104,16 @@ export class DeviceFileSystem {
|
|
|
94
104
|
registerEntryAuthority(this, async () => "unknown");
|
|
95
105
|
return view;
|
|
96
106
|
}
|
|
97
|
-
#resolve(path, options, followFinal = true, resizeCreate) {
|
|
98
|
-
return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate);
|
|
107
|
+
#resolve(path, options, followFinal = true, resizeCreate, traversal) {
|
|
108
|
+
return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate, traversal);
|
|
109
|
+
}
|
|
110
|
+
#tryResolveSync(path, options) {
|
|
111
|
+
if (options.signal?.aborted)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (Reflect.get(this.#filesystem, pathNamespace) === undefined) {
|
|
114
|
+
return tryResolveMemoryDevicePath(this.#filesystem, path);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
99
117
|
}
|
|
100
118
|
async #mutable(path, options, followFinal = true) {
|
|
101
119
|
if (reserved(await this.#resolve(path, options, followFinal)))
|
|
@@ -129,10 +147,14 @@ export class DeviceFileSystem {
|
|
|
129
147
|
return reserved(resolved) ? resolved : this.#filesystem.canonicalizeMissingTarget?.(path, options);
|
|
130
148
|
}
|
|
131
149
|
async capabilitiesFor(path, options = {}) {
|
|
150
|
+
options.signal?.throwIfAborted();
|
|
151
|
+
if (options.stagingResolution === true && (normalizePath(path) !== path || !path.startsWith("/")))
|
|
152
|
+
throw new FsError("EINVAL", { path });
|
|
132
153
|
let resolved;
|
|
133
154
|
let selected;
|
|
155
|
+
const traversal = options.stagingResolution === true ? { virtual: false } : undefined;
|
|
134
156
|
try {
|
|
135
|
-
resolved = await this.#resolve(path, options, options.create !== undefined || options.creation !== "exclusive", options.create);
|
|
157
|
+
resolved = await this.#resolve(path, options, options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"), options.create, traversal);
|
|
136
158
|
}
|
|
137
159
|
catch (error) {
|
|
138
160
|
options.signal?.throwIfAborted();
|
|
@@ -160,6 +182,10 @@ export class DeviceFileSystem {
|
|
|
160
182
|
return { ...deviceCapabilities, open: false, readdir: true, write: false, append: false,
|
|
161
183
|
exclusiveCreate: false, streamingWrite: false, streamingAppend: false, independentWriteStreams: false, descriptorWriteStream: false,
|
|
162
184
|
retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
|
|
185
|
+
// This intent cannot bind virtual traversal. Do not infer other path
|
|
186
|
+
// capabilities by resolving the shadowed entry in the backing filesystem.
|
|
187
|
+
if (traversal?.virtual)
|
|
188
|
+
return Object.freeze({ synchronousStagingResolution: false });
|
|
163
189
|
if (selected === undefined) {
|
|
164
190
|
const query = this.#filesystem.capabilitiesFor;
|
|
165
191
|
options.signal?.throwIfAborted();
|
|
@@ -169,26 +195,58 @@ export class DeviceFileSystem {
|
|
|
169
195
|
const observed = ownedMutationCapabilities(this.#filesystem, selected ?? this.#filesystem.capabilities);
|
|
170
196
|
options.signal?.throwIfAborted();
|
|
171
197
|
const capabilities = observed.retainedResize === true ? retainedResizeCapabilities(this.#filesystem, observed) : observed;
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
let unavailable;
|
|
199
|
+
if (options.stagingResolution === true && observed.synchronousStagingResolution === true) {
|
|
200
|
+
try {
|
|
201
|
+
const resolution = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, options));
|
|
202
|
+
options.signal?.throwIfAborted();
|
|
203
|
+
if (resolution.path !== resolved)
|
|
204
|
+
throw new FsError("EAGAIN", { path });
|
|
205
|
+
if ([resolution.path, ...resolution.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
|
|
206
|
+
(unavailable ??= {}).synchronousStagingResolution = false;
|
|
207
|
+
}
|
|
208
|
+
runStagingGuard(resolution.validate);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
options.signal?.throwIfAborted();
|
|
212
|
+
if (!isFsError(error, "ENOTSUP"))
|
|
213
|
+
throw error;
|
|
214
|
+
(unavailable ??= {}).synchronousStagingResolution = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (resolved.startsWith(`${deviceDirectory}/`)) {
|
|
218
|
+
const u = unavailable ??= {};
|
|
219
|
+
u.atomicStagingAncestry = false;
|
|
220
|
+
u.synchronousDirectoryValidation = false;
|
|
221
|
+
u.synchronousStagingResolution = false;
|
|
222
|
+
}
|
|
223
|
+
if (typeof this.#filesystem.open !== "function" && capabilities.open !== false)
|
|
224
|
+
(unavailable ??= {}).open = false;
|
|
225
|
+
if (typeof this.#filesystem.readStream !== "function" && capabilities.streamingRead !== false)
|
|
226
|
+
(unavailable ??= {}).streamingRead = false;
|
|
177
227
|
if (typeof this.#filesystem.writeStream !== "function") {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
228
|
+
if (capabilities.streamingWrite !== false)
|
|
229
|
+
(unavailable ??= {}).streamingWrite = false;
|
|
230
|
+
if (capabilities.streamingAppend !== false)
|
|
231
|
+
(unavailable ??= {}).streamingAppend = false;
|
|
232
|
+
if (capabilities.descriptorWriteStream !== false)
|
|
233
|
+
(unavailable ??= {}).descriptorWriteStream = false;
|
|
234
|
+
}
|
|
235
|
+
if (typeof this.#filesystem.openReadFile !== "function" && capabilities.retainedRead !== false)
|
|
236
|
+
(unavailable ??= {}).retainedRead = false;
|
|
237
|
+
if (observed.atomicResize === true && (typeof this.#filesystem.resizeFile !== "function" || observed.readOnly === true) && capabilities.atomicResize !== false)
|
|
238
|
+
(unavailable ??= {}).atomicResize = false;
|
|
239
|
+
const result = unavailable ? { ...capabilities, ...unavailable } : capabilities;
|
|
188
240
|
options.signal?.throwIfAborted();
|
|
189
241
|
return result;
|
|
190
242
|
}
|
|
191
|
-
|
|
243
|
+
stat(path, options = {}) {
|
|
244
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
245
|
+
return this.#filesystem.stat(path, options);
|
|
246
|
+
}
|
|
247
|
+
return this.#statSlow(path, options);
|
|
248
|
+
}
|
|
249
|
+
async #statSlow(path, options) {
|
|
192
250
|
const resolved = await this.#resolve(path, options);
|
|
193
251
|
options.signal?.throwIfAborted();
|
|
194
252
|
if (resolved === nullPath)
|
|
@@ -201,7 +259,13 @@ export class DeviceFileSystem {
|
|
|
201
259
|
options.signal?.throwIfAborted();
|
|
202
260
|
return result;
|
|
203
261
|
}
|
|
204
|
-
|
|
262
|
+
lstat(path, options = {}) {
|
|
263
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
264
|
+
return this.#filesystem.lstat(path, options);
|
|
265
|
+
}
|
|
266
|
+
return this.#lstatSlow(path, options);
|
|
267
|
+
}
|
|
268
|
+
async #lstatSlow(path, options) {
|
|
205
269
|
const resolved = await this.#resolve(path, options, false);
|
|
206
270
|
options.signal?.throwIfAborted();
|
|
207
271
|
if (resolved === nullPath)
|
|
@@ -217,11 +281,23 @@ export class DeviceFileSystem {
|
|
|
217
281
|
compareEntry(path, peer, peerPath, options = {}) {
|
|
218
282
|
return compareEntries(this, path, peer, peerPath, options);
|
|
219
283
|
}
|
|
220
|
-
|
|
284
|
+
realpath(path, options = {}) {
|
|
285
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
286
|
+
return this.#filesystem.realpath(path, options);
|
|
287
|
+
}
|
|
288
|
+
return this.#realpathSlow(path, options);
|
|
289
|
+
}
|
|
290
|
+
async #realpathSlow(path, options) {
|
|
221
291
|
const resolved = await this.#resolve(path, options);
|
|
222
292
|
return reserved(resolved) ? resolved : this.#filesystem.realpath(path, options);
|
|
223
293
|
}
|
|
224
|
-
|
|
294
|
+
readFile(path, options = {}) {
|
|
295
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
296
|
+
return this.#filesystem.readFile(path, options);
|
|
297
|
+
}
|
|
298
|
+
return this.#readFileSlow(path, options);
|
|
299
|
+
}
|
|
300
|
+
async #readFileSlow(path, options) {
|
|
225
301
|
const resolved = await this.#resolve(path, options);
|
|
226
302
|
if (resolved === nullPath) {
|
|
227
303
|
nonnegative(options.maxBytes, path);
|
|
@@ -389,7 +465,14 @@ export class DeviceFileSystem {
|
|
|
389
465
|
throw new FsError("ENOTSUP", { path });
|
|
390
466
|
await this.#filesystem.writeStream(path, source, options);
|
|
391
467
|
}
|
|
392
|
-
|
|
468
|
+
readdir(path, options = {}) {
|
|
469
|
+
const fast = this.#tryResolveSync(path, options);
|
|
470
|
+
if (fast !== undefined && fast !== "/") {
|
|
471
|
+
return this.#filesystem.readdir(path, options);
|
|
472
|
+
}
|
|
473
|
+
return this.#readdirSlow(path, options);
|
|
474
|
+
}
|
|
475
|
+
async #readdirSlow(path, options) {
|
|
393
476
|
const resolved = await this.#resolve(path, options);
|
|
394
477
|
if (resolved === nullPath)
|
|
395
478
|
throw new FsError("ENOTDIR", { path });
|
|
@@ -421,7 +504,13 @@ export class DeviceFileSystem {
|
|
|
421
504
|
admitDirectoryEntries(merged.size, limit, path);
|
|
422
505
|
return [...merged.values()];
|
|
423
506
|
}
|
|
424
|
-
|
|
507
|
+
mkdir(path, options = {}) {
|
|
508
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
509
|
+
return this.#filesystem.mkdir(path, options);
|
|
510
|
+
}
|
|
511
|
+
return this.#mkdirSlow(path, options);
|
|
512
|
+
}
|
|
513
|
+
async #mkdirSlow(path, options) {
|
|
425
514
|
const resolved = await this.#resolve(path, options);
|
|
426
515
|
if (reserved(resolved)) {
|
|
427
516
|
if (resolved !== nullPath && options.recursive)
|
|
@@ -430,7 +519,13 @@ export class DeviceFileSystem {
|
|
|
430
519
|
}
|
|
431
520
|
await this.#filesystem.mkdir(path, options);
|
|
432
521
|
}
|
|
433
|
-
|
|
522
|
+
rm(path, options = {}) {
|
|
523
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
524
|
+
return this.#filesystem.rm(path, options);
|
|
525
|
+
}
|
|
526
|
+
return this.#rmSlow(path, options);
|
|
527
|
+
}
|
|
528
|
+
async #rmSlow(path, options) {
|
|
434
529
|
await this.#mutable(path, options, false);
|
|
435
530
|
await this.#filesystem.rm(path, options);
|
|
436
531
|
}
|
|
@@ -466,11 +561,16 @@ export class DeviceFileSystem {
|
|
|
466
561
|
return this.#filesystem.writeFileConditional(path, data, options);
|
|
467
562
|
}
|
|
468
563
|
async removeEntryConditional(path, options) {
|
|
564
|
+
options = { ...options };
|
|
565
|
+
const requested = options.returnRemainingStat === true;
|
|
469
566
|
await this.#mutable(path, options, false);
|
|
470
567
|
await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemoval", options);
|
|
568
|
+
if (requested)
|
|
569
|
+
await requireOwnedMutation(this.#filesystem, path, "atomicEntryRemovalReceipt", options);
|
|
471
570
|
if (!this.#filesystem.removeEntryConditional)
|
|
472
571
|
throw new FsError("ENOTSUP", { path });
|
|
473
|
-
await this.#filesystem.removeEntryConditional(path, options);
|
|
572
|
+
const receipt = await this.#filesystem.removeEntryConditional(path, options);
|
|
573
|
+
return requested && receipt !== undefined ? Object.freeze({ ...receipt }) : undefined;
|
|
474
574
|
}
|
|
475
575
|
async removeFileConditional(path, options) {
|
|
476
576
|
await this.#mutable(path, options, false);
|
|
@@ -487,20 +587,95 @@ export class DeviceFileSystem {
|
|
|
487
587
|
await this.#filesystem.removeTreeConditional(path, options);
|
|
488
588
|
}
|
|
489
589
|
async createStagedFile(path, name, content, options) {
|
|
590
|
+
options = snapshotStagingCreation(options, path);
|
|
490
591
|
await this.#mutable(path, options, false);
|
|
491
592
|
await requireOwnedMutation(this.#filesystem, path, "atomicFileStaging", options, true);
|
|
593
|
+
if (options.retainCleanup === true)
|
|
594
|
+
await requireOwnedMutation(this.#filesystem, path, "retainedStagingCleanup", options, true);
|
|
492
595
|
if (!this.#filesystem.createStagedFile)
|
|
493
596
|
throw new FsError("ENOTSUP", { path });
|
|
494
597
|
return this.#filesystem.createStagedFile(path, name, content, options);
|
|
495
598
|
}
|
|
496
599
|
async publishStagedFile(staging, destination, options) {
|
|
600
|
+
options.signal?.throwIfAborted();
|
|
601
|
+
if (options.ancestors !== undefined) {
|
|
602
|
+
const entries = snapshotDirectoryAncestry(options.ancestors);
|
|
603
|
+
if (entries.some(entry => entry.path === deviceDirectory || entry.path.startsWith(`${deviceDirectory}/`)))
|
|
604
|
+
throw new FsError("ENOTSUP", { path: destination });
|
|
605
|
+
await requireOwnedMutation(this.#filesystem, destination, "atomicStagingAncestry", options, options.destination === null);
|
|
606
|
+
}
|
|
497
607
|
for (const path of [staging.directory.path, staging.file.path, destination])
|
|
498
608
|
await this.#mutable(path, options, false);
|
|
499
609
|
await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
|
|
610
|
+
await requireOwnedMutation(this.#filesystem, destination, "atomicFileStaging", options, options.destination === null);
|
|
500
611
|
if (!this.#filesystem.publishStagedFile)
|
|
501
612
|
throw new FsError("ENOTSUP", { path: destination });
|
|
613
|
+
if (options.commitGuard !== undefined)
|
|
614
|
+
await requireOwnedMutation(this.#filesystem, destination, "guardedStagingPublication", options, options.destination === null);
|
|
502
615
|
await this.#filesystem.publishStagedFile(staging, destination, options);
|
|
503
616
|
}
|
|
617
|
+
async prepareDirectoryAncestry(ancestors, options = {}) {
|
|
618
|
+
options.signal?.throwIfAborted();
|
|
619
|
+
try {
|
|
620
|
+
const entries = snapshotDirectoryAncestry(ancestors);
|
|
621
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
622
|
+
controls.signal?.throwIfAborted();
|
|
623
|
+
if (entries.some(entry => entry.path === deviceDirectory || entry.path.startsWith(`${deviceDirectory}/`))
|
|
624
|
+
|| !this.#filesystem.prepareDirectoryAncestry) {
|
|
625
|
+
throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry" });
|
|
626
|
+
}
|
|
627
|
+
for (const entry of entries) {
|
|
628
|
+
const declared = await this.#filesystem.capabilitiesFor?.(entry.path, { ...controls, stagingAncestry: true }) ?? this.#filesystem.capabilities;
|
|
629
|
+
controls.signal?.throwIfAborted();
|
|
630
|
+
if (declared.synchronousDirectoryValidation !== true)
|
|
631
|
+
throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry", path: entry.path });
|
|
632
|
+
}
|
|
633
|
+
const guard = await this.#filesystem.prepareDirectoryAncestry(entries, controls);
|
|
634
|
+
controls.signal?.throwIfAborted();
|
|
635
|
+
if (typeof guard !== "function")
|
|
636
|
+
throw new FsError("ENOTSUP", { syscall: "prepareDirectoryAncestry" });
|
|
637
|
+
return () => {
|
|
638
|
+
controls.signal?.throwIfAborted();
|
|
639
|
+
runStagingGuard(guard);
|
|
640
|
+
return true;
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
options.signal?.throwIfAborted();
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async prepareStagingResolution(path, options = {}) {
|
|
649
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
650
|
+
controls.signal?.throwIfAborted();
|
|
651
|
+
if (normalizePath(path) !== path || !path.startsWith("/"))
|
|
652
|
+
throw new FsError("EINVAL", { path });
|
|
653
|
+
try {
|
|
654
|
+
const traversal = { virtual: false };
|
|
655
|
+
const resolved = await this.#resolve(path, controls, false, undefined, traversal);
|
|
656
|
+
controls.signal?.throwIfAborted();
|
|
657
|
+
if (traversal.virtual)
|
|
658
|
+
throw new FsError("ENOTSUP", { path });
|
|
659
|
+
const declared = ownedMutationCapabilities(this.#filesystem, await this.#filesystem.capabilitiesFor?.(path, { ...controls, stagingResolution: true }) ?? this.#filesystem.capabilities);
|
|
660
|
+
controls.signal?.throwIfAborted();
|
|
661
|
+
if (declared.synchronousStagingResolution !== true || !this.#filesystem.prepareStagingResolution)
|
|
662
|
+
throw new FsError("ENOTSUP", { path });
|
|
663
|
+
const receipt = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, controls));
|
|
664
|
+
controls.signal?.throwIfAborted();
|
|
665
|
+
if (receipt.path !== resolved)
|
|
666
|
+
throw new FsError("EAGAIN", { path });
|
|
667
|
+
if ([receipt.path, ...receipt.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
|
|
668
|
+
throw new FsError("ENOTSUP", { path });
|
|
669
|
+
}
|
|
670
|
+
const validate = () => { controls.signal?.throwIfAborted(); runStagingGuard(receipt.validate); return true; };
|
|
671
|
+
runStagingGuard(validate);
|
|
672
|
+
return snapshotStagingResolution({ ...receipt, validate });
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
controls.signal?.throwIfAborted();
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
504
679
|
async removeStagedFile(staging, options = {}) {
|
|
505
680
|
for (const path of [staging.directory.path, staging.file.path])
|
|
506
681
|
await this.#mutable(path, options, false);
|
|
@@ -558,7 +733,13 @@ export class DeviceFileSystem {
|
|
|
558
733
|
throw new FsError("EISDIR", { path: destination });
|
|
559
734
|
await this.#filesystem.writeFile(destination, new Uint8Array(), { ...options, flag: options.exclusive ? "wx" : "w" });
|
|
560
735
|
}
|
|
561
|
-
|
|
736
|
+
access(path, mode = 0, options = {}) {
|
|
737
|
+
if (this.#tryResolveSync(path, options) !== undefined) {
|
|
738
|
+
return this.#filesystem.access(path, mode, options);
|
|
739
|
+
}
|
|
740
|
+
return this.#accessSlow(path, mode, options);
|
|
741
|
+
}
|
|
742
|
+
async #accessSlow(path, mode, options) {
|
|
562
743
|
const resolved = await this.#resolve(path, options);
|
|
563
744
|
if (resolved === nullPath || resolved === deviceDirectory) {
|
|
564
745
|
if (!Number.isInteger(mode) || mode < 0 || mode > 7)
|
|
@@ -2,4 +2,6 @@ import type { FileSystem, FsOptions } from "../../contracts/filesystem.js";
|
|
|
2
2
|
export declare const nullPath = "/dev/null";
|
|
3
3
|
export declare const deviceDirectory = "/dev";
|
|
4
4
|
export declare function lexicalDevicePath(path: string): string;
|
|
5
|
-
export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean
|
|
5
|
+
export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean, traversal?: {
|
|
6
|
+
virtual: boolean;
|
|
7
|
+
}): Promise<string>;
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
2
|
-
import { validatePath } from "../../contracts/virtual-path.js";
|
|
3
|
-
import { capturePathNamespace } from "../path-namespace.js";
|
|
2
|
+
import { MAX_PATH_COMPONENTS, validatePath } from "../../contracts/virtual-path.js";
|
|
3
|
+
import { capturePathNamespace, pathNamespace } from "../path-namespace.js";
|
|
4
|
+
import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../memory/index.js";
|
|
4
5
|
export const nullPath = "/dev/null";
|
|
5
6
|
export const deviceDirectory = "/dev";
|
|
7
|
+
function isNullParts(parts) {
|
|
8
|
+
return parts.length === 2 && parts[0] === "dev" && parts[1] === "null";
|
|
9
|
+
}
|
|
6
10
|
export function lexicalDevicePath(path) {
|
|
7
11
|
validatePath(path);
|
|
12
|
+
if (isCleanAbsolutePath(path) && !path.startsWith("/dev/null/"))
|
|
13
|
+
return path;
|
|
8
14
|
const parts = [];
|
|
9
15
|
for (const component of path.split("/")) {
|
|
10
|
-
if (
|
|
16
|
+
if (isNullParts(parts))
|
|
11
17
|
throw new FsError("ENOTDIR", { path });
|
|
12
18
|
if (component === "..")
|
|
13
19
|
parts.pop();
|
|
@@ -16,7 +22,7 @@ export function lexicalDevicePath(path) {
|
|
|
16
22
|
}
|
|
17
23
|
return `/${parts.join("/")}`;
|
|
18
24
|
}
|
|
19
|
-
async function resolveResizeDevicePath(filesystem, path, options, create, namespace) {
|
|
25
|
+
async function resolveResizeDevicePath(filesystem, path, options, create, namespace, traversal) {
|
|
20
26
|
validatePath(path);
|
|
21
27
|
const components = (value) => {
|
|
22
28
|
const result = value.split("/").filter(Boolean);
|
|
@@ -25,6 +31,9 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
|
|
|
25
31
|
return result;
|
|
26
32
|
};
|
|
27
33
|
const pending = components(path);
|
|
34
|
+
let remainingComponents = MAX_PATH_COMPONENTS - pending.length;
|
|
35
|
+
if (remainingComponents < 0)
|
|
36
|
+
throw new FsError("ENAMETOOLONG", { path });
|
|
28
37
|
const parts = [];
|
|
29
38
|
let links = 0;
|
|
30
39
|
let expanded = path.length;
|
|
@@ -57,12 +66,14 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
|
|
|
57
66
|
parts.pop();
|
|
58
67
|
continue;
|
|
59
68
|
}
|
|
60
|
-
if (
|
|
69
|
+
if (exceedsComponentByteLimit(component))
|
|
61
70
|
throw new FsError("ENAMETOOLONG", { path });
|
|
62
71
|
const candidate = `/${[...parts, component].join("/")}`;
|
|
63
72
|
const selected = namespace === undefined ? undefined : candidate === deviceDirectory || candidate === nullPath ? "/" : namespace(candidate);
|
|
64
73
|
if (boundary !== undefined && selected !== boundary)
|
|
65
74
|
throw new FsError("EACCES", { path });
|
|
75
|
+
if (traversal && (candidate === deviceDirectory || candidate === nullPath))
|
|
76
|
+
traversal.virtual = true;
|
|
66
77
|
if (create && pending.length === 1 && pending[0] === "")
|
|
67
78
|
throw new FsError("EISDIR", { path });
|
|
68
79
|
if (candidate !== deviceDirectory && candidate !== nullPath) {
|
|
@@ -97,6 +108,9 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
|
|
|
97
108
|
const targetParts = components(target);
|
|
98
109
|
if (targetParts.at(-1) === "" && pending[0] === "")
|
|
99
110
|
targetParts.pop();
|
|
111
|
+
if (targetParts.length > remainingComponents)
|
|
112
|
+
throw new FsError("ENAMETOOLONG", { path });
|
|
113
|
+
remainingComponents -= targetParts.length;
|
|
100
114
|
pending.unshift(...targetParts);
|
|
101
115
|
continue;
|
|
102
116
|
}
|
|
@@ -105,11 +119,36 @@ async function resolveResizeDevicePath(filesystem, path, options, create, namesp
|
|
|
105
119
|
}
|
|
106
120
|
return `/${parts.join("/")}`;
|
|
107
121
|
}
|
|
108
|
-
|
|
122
|
+
function utf8ByteLength(value) {
|
|
123
|
+
let bytes = 0;
|
|
124
|
+
for (let i = 0; i < value.length; i++) {
|
|
125
|
+
const code = value.charCodeAt(i);
|
|
126
|
+
if (code < 0x80)
|
|
127
|
+
bytes += 1;
|
|
128
|
+
else if (code < 0x800)
|
|
129
|
+
bytes += 2;
|
|
130
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
|
|
131
|
+
bytes += 4;
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
bytes += 3;
|
|
136
|
+
}
|
|
137
|
+
return bytes;
|
|
138
|
+
}
|
|
139
|
+
function exceedsComponentByteLimit(value) {
|
|
140
|
+
return value.length > 85 && (value.length > 255 || utf8ByteLength(value) > 255);
|
|
141
|
+
}
|
|
142
|
+
export async function resolveDevicePath(filesystem, path, options, followFinal = true, resizeCreate, traversal) {
|
|
109
143
|
options.signal?.throwIfAborted();
|
|
144
|
+
if (Reflect.get(filesystem, pathNamespace) === undefined) {
|
|
145
|
+
const fast = tryResolveMemoryDevicePath(filesystem, path, resizeCreate);
|
|
146
|
+
if (fast !== undefined)
|
|
147
|
+
return fast;
|
|
148
|
+
}
|
|
110
149
|
const namespace = capturePathNamespace(filesystem, options);
|
|
111
150
|
if (resizeCreate !== undefined)
|
|
112
|
-
return resolveResizeDevicePath(filesystem, path, options, resizeCreate, namespace);
|
|
151
|
+
return resolveResizeDevicePath(filesystem, path, options, resizeCreate, namespace, traversal);
|
|
113
152
|
const lexical = lexicalDevicePath(path);
|
|
114
153
|
const lstat = filesystem.lstat;
|
|
115
154
|
options.signal?.throwIfAborted();
|
|
@@ -117,15 +156,19 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
117
156
|
if (!aliases && lexical !== nullPath && lexical !== deviceDirectory && lexical !== "/")
|
|
118
157
|
return lexical;
|
|
119
158
|
const pending = path.split("/");
|
|
159
|
+
let remainingComponents = MAX_PATH_COMPONENTS - pending.filter(Boolean).length;
|
|
160
|
+
if (remainingComponents < 0)
|
|
161
|
+
throw new FsError("ENAMETOOLONG", { path });
|
|
120
162
|
const parts = [];
|
|
121
163
|
let links = 0;
|
|
122
164
|
let expanded = path.length;
|
|
123
165
|
let absolute = path.startsWith("/");
|
|
124
166
|
let traversalFailure;
|
|
167
|
+
let failedDepth = Infinity;
|
|
125
168
|
let boundary;
|
|
126
169
|
while (pending.length) {
|
|
127
170
|
options.signal?.throwIfAborted();
|
|
128
|
-
if (
|
|
171
|
+
if (isNullParts(parts))
|
|
129
172
|
throw new FsError("ENOTDIR", { path });
|
|
130
173
|
const component = pending.shift();
|
|
131
174
|
if (!component || component === ".")
|
|
@@ -134,13 +177,18 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
134
177
|
if (boundary !== undefined && `/${parts.join("/")}` === boundary)
|
|
135
178
|
throw new FsError("EACCES", { path });
|
|
136
179
|
parts.pop();
|
|
180
|
+
if (parts.length < failedDepth) {
|
|
181
|
+
failedDepth = Infinity;
|
|
182
|
+
}
|
|
137
183
|
continue;
|
|
138
184
|
}
|
|
139
185
|
const candidate = `/${[...parts, component].join("/")}`;
|
|
140
186
|
const selected = namespace === undefined ? undefined : candidate === deviceDirectory || candidate === nullPath ? "/" : namespace(candidate);
|
|
141
187
|
if (boundary !== undefined && selected !== boundary)
|
|
142
188
|
throw new FsError("EACCES", { path });
|
|
143
|
-
if (candidate
|
|
189
|
+
if (traversal && (candidate === deviceDirectory || candidate === nullPath))
|
|
190
|
+
traversal.virtual = true;
|
|
191
|
+
if (parts.length < failedDepth && candidate !== deviceDirectory && candidate !== nullPath && ((followFinal && aliases) || pending.length)) {
|
|
144
192
|
const lookup = absolute ? candidate : candidate.slice(1);
|
|
145
193
|
let stat;
|
|
146
194
|
try {
|
|
@@ -154,8 +202,10 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
154
202
|
return lexical;
|
|
155
203
|
if (!isFsError(error, "ENOENT"))
|
|
156
204
|
throw error;
|
|
157
|
-
if (pending.length)
|
|
205
|
+
if (pending.length) {
|
|
158
206
|
traversalFailure ??= error;
|
|
207
|
+
failedDepth = Math.min(failedDepth, parts.length + 1);
|
|
208
|
+
}
|
|
159
209
|
}
|
|
160
210
|
options.signal?.throwIfAborted();
|
|
161
211
|
if (stat?.type === "symlink") {
|
|
@@ -172,15 +222,23 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
172
222
|
expanded += target.length;
|
|
173
223
|
if (expanded > 65536)
|
|
174
224
|
throw new FsError("ENAMETOOLONG", { path });
|
|
225
|
+
const targetSplit = target.split("/");
|
|
226
|
+
const nonEmptyCount = targetSplit.filter(Boolean).length;
|
|
227
|
+
if (nonEmptyCount > remainingComponents)
|
|
228
|
+
throw new FsError("ENAMETOOLONG", { path });
|
|
229
|
+
remainingComponents -= nonEmptyCount;
|
|
175
230
|
if (target.startsWith("/")) {
|
|
176
231
|
parts.splice(0, parts.length, ...(boundary ?? "/").split("/").filter(Boolean));
|
|
177
232
|
absolute = true;
|
|
233
|
+
failedDepth = Infinity;
|
|
178
234
|
}
|
|
179
|
-
pending.unshift(...
|
|
235
|
+
pending.unshift(...targetSplit);
|
|
180
236
|
continue;
|
|
181
237
|
}
|
|
182
|
-
if (stat && pending.length && stat.type !== "directory")
|
|
238
|
+
if (stat && pending.length && stat.type !== "directory") {
|
|
183
239
|
traversalFailure ??= new FsError("ENOTDIR", { path });
|
|
240
|
+
failedDepth = Math.min(failedDepth, parts.length + 1);
|
|
241
|
+
}
|
|
184
242
|
}
|
|
185
243
|
parts.push(component);
|
|
186
244
|
}
|