@poe-platform/safe-fs 0.1.721 → 0.1.723
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 +67 -21
- package/dist/safe-fs/bridge/filesystem.d.ts +4 -0
- package/dist/safe-fs/bridge/filesystem.js +49 -10
- package/dist/safe-fs/bridge/types.d.ts +6 -0
- package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +15 -0
- package/dist/safe-fs/contracts/io.d.ts +3 -1
- package/dist/safe-fs/contracts/io.js +48 -16
- package/dist/safe-fs/core.d.ts +2 -1
- package/dist/safe-fs/core.js +1 -1
- package/dist/safe-fs/fs/capabilities.js +6 -3
- package/dist/safe-fs/fs/descriptor.js +9 -2
- package/dist/safe-fs/fs/devices/index.d.ts +1 -0
- package/dist/safe-fs/fs/devices/index.js +24 -6
- package/dist/safe-fs/fs/memory/index.d.ts +2 -0
- package/dist/safe-fs/fs/memory/index.js +95 -4
- package/dist/safe-fs/fs/memory/limits.js +6 -4
- package/dist/safe-fs/fs/mount/index.d.ts +4 -0
- package/dist/safe-fs/fs/mount/index.js +76 -12
- package/dist/safe-fs/fs/object-publication/index.d.ts +4 -0
- package/dist/safe-fs/fs/object-publication/index.js +9 -8
- package/dist/safe-fs/fs/overlay/index.d.ts +4 -2
- package/dist/safe-fs/fs/overlay/index.js +92 -36
- package/dist/safe-fs/fs/quota/index.js +83 -23
- package/dist/safe-fs/fs/readonly/index.js +5 -3
- package/dist/safe-fs/fs/real/index.d.ts +21 -1
- package/dist/safe-fs/fs/real/index.js +266 -22
- package/dist/safe-fs/fs/s3/filesystem.d.ts +8 -0
- package/dist/safe-fs/fs/s3/filesystem.js +59 -12
- package/dist/safe-fs/fs/s3/http/request.d.ts +1 -1
- package/dist/safe-fs/fs/s3/http/request.js +1 -1
- package/dist/safe-fs/fs/s3/http/transport.js +8 -6
- package/dist/safe-fs/fs/s3/http/xml.d.ts +4 -1
- package/dist/safe-fs/fs/s3/http/xml.js +6 -2
- package/dist/safe-fs/fs/s3/manifest-admission.d.ts +8 -0
- package/dist/safe-fs/fs/s3/manifest-admission.js +171 -0
- package/dist/safe-fs/fs/s3/namespace.js +13 -10
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +54 -35
- package/dist/safe-fs/fs/webdav/webdav.d.ts +4 -1
- package/dist/safe-fs/fs/webdav/webdav.js +101 -23
- package/dist/safe-fs/node/filesystem.d.ts +8 -1
- package/dist/safe-fs/node/filesystem.js +7 -1
- package/dist/safe-fs/node/host.d.ts +3 -0
- package/dist/safe-fs/node/host.js +78 -0
- package/dist/safe-fs/node/index.d.ts +2 -1
- package/dist/safe-fs/node/index.js +2 -1
- package/dist/safe-fs/platform/browser.d.ts +1 -0
- package/dist/safe-fs/platform/browser.js +1 -0
- package/dist/safe-fs/platform/node.d.ts +1 -0
- package/dist/safe-fs/platform/node.js +1 -0
- package/dist/safe-fs/python/emscripten.d.ts +1 -0
- package/dist/safe-fs/python/emscripten.js +5 -5
- package/dist/safe-fs/python/filesystem.js +5 -5
- package/dist/safe-fs/python/native.d.ts +1 -0
- package/dist/safe-fs/python/native.js +3 -2
- package/dist/safe-fs/xml.js +32 -21
- package/package.json +1 -1
|
@@ -39,6 +39,7 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
39
39
|
access(path: string, mode?: number, options?: FsOptions): Promise<void>;
|
|
40
40
|
readlink(path: string, options?: FsOptions): Promise<string>;
|
|
41
41
|
symlink(target: string, path: string, options?: FsOptions): Promise<void>;
|
|
42
|
+
confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
42
43
|
link(existingPath: string, newPath: string, options?: FsOptions): Promise<void>;
|
|
43
44
|
chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
|
|
44
45
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|
|
@@ -15,13 +15,14 @@ const deviceCapabilities = Object.freeze({
|
|
|
15
15
|
remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
|
|
16
16
|
mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
|
|
17
17
|
permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
|
|
18
|
-
open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
|
|
18
|
+
open: true, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
19
19
|
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
|
|
20
20
|
});
|
|
21
21
|
function globalCapabilities(filesystem) {
|
|
22
22
|
const capabilities = { readOnly: false };
|
|
23
23
|
const optional = {
|
|
24
24
|
open: ["open"],
|
|
25
|
+
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
25
26
|
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
26
27
|
streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
|
|
27
28
|
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
@@ -101,7 +102,16 @@ export class DeviceFileSystem {
|
|
|
101
102
|
throw new FsError("EBUSY", { path });
|
|
102
103
|
}
|
|
103
104
|
async #writeTarget(path, options) {
|
|
104
|
-
|
|
105
|
+
let resolved;
|
|
106
|
+
try {
|
|
107
|
+
resolved = await this.#resolve(path, options);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
options.signal?.throwIfAborted();
|
|
111
|
+
if ((options.flag === "wx" || options.flag === "ax") && isFsError(error, "ELOOP"))
|
|
112
|
+
return false;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
105
115
|
if (resolved === nullPath) {
|
|
106
116
|
if (options.flag === "wx" || options.flag === "ax")
|
|
107
117
|
throw new FsError("EEXIST", { path });
|
|
@@ -122,7 +132,7 @@ export class DeviceFileSystem {
|
|
|
122
132
|
let resolved;
|
|
123
133
|
let selected;
|
|
124
134
|
try {
|
|
125
|
-
resolved = await this.#resolve(path, options,
|
|
135
|
+
resolved = await this.#resolve(path, options, options.create !== undefined || options.creation !== "exclusive", options.create);
|
|
126
136
|
}
|
|
127
137
|
catch (error) {
|
|
128
138
|
options.signal?.throwIfAborted();
|
|
@@ -261,11 +271,11 @@ export class DeviceFileSystem {
|
|
|
261
271
|
options?.signal?.throwIfAborted();
|
|
262
272
|
if (!options || typeof options !== "object")
|
|
263
273
|
throw new FsError("EINVAL", { syscall: "open", path });
|
|
264
|
-
const resolved = await this.#resolve(path, options, options.creation !== "exclusive");
|
|
274
|
+
const resolved = await this.#resolve(path, options, !options.noFollow && options.creation !== "exclusive");
|
|
265
275
|
options.signal?.throwIfAborted();
|
|
266
276
|
if (resolved === nullPath)
|
|
267
277
|
return openFileDescriptor(path, options, {
|
|
268
|
-
position: true, positionedRead: true, positionedWrite: true, positionedAppendWrite: true,
|
|
278
|
+
noFollow: true, position: true, positionedRead: true, positionedWrite: true, positionedAppendWrite: true,
|
|
269
279
|
openTruncate: true, delegateZeroLengthWrite: true, truncate: false, synchronization: "none",
|
|
270
280
|
}, async (admitted) => {
|
|
271
281
|
if (admitted.creation === "exclusive")
|
|
@@ -289,7 +299,7 @@ export class DeviceFileSystem {
|
|
|
289
299
|
throw new FsError("ENOTSUP", { syscall: "open", path });
|
|
290
300
|
const query = this.#filesystem.capabilitiesFor;
|
|
291
301
|
options.signal?.throwIfAborted();
|
|
292
|
-
const capabilities = query && options.creation !== "exclusive" ? await Reflect.apply(query, this.#filesystem, [path, options]) : this.#filesystem.capabilities;
|
|
302
|
+
const capabilities = query && !options.noFollow && options.creation !== "exclusive" ? await Reflect.apply(query, this.#filesystem, [path, options]) : this.#filesystem.capabilities;
|
|
293
303
|
options.signal?.throwIfAborted();
|
|
294
304
|
const open = this.#filesystem.open;
|
|
295
305
|
options.signal?.throwIfAborted();
|
|
@@ -572,6 +582,14 @@ export class DeviceFileSystem {
|
|
|
572
582
|
throw new FsError("ENOTSUP", { path });
|
|
573
583
|
await this.#filesystem.symlink(target, path, options);
|
|
574
584
|
}
|
|
585
|
+
async confineExtraction(roots, options = {}) {
|
|
586
|
+
options.signal?.throwIfAborted();
|
|
587
|
+
if (!this.#filesystem.confineExtraction)
|
|
588
|
+
throw new FsError("ENOTSUP", { syscall: "confineExtraction" });
|
|
589
|
+
// Extraction uses only the backing namespace; synthetic devices must never
|
|
590
|
+
// become archive publication destinations.
|
|
591
|
+
return this.#filesystem.confineExtraction(roots, options);
|
|
592
|
+
}
|
|
575
593
|
async link(existingPath, newPath, options = {}) {
|
|
576
594
|
await this.#mutable(existingPath, options, false);
|
|
577
595
|
await this.#mutable(newPath, options, false);
|
|
@@ -35,6 +35,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
35
35
|
timestamps: true;
|
|
36
36
|
atomicRename: true;
|
|
37
37
|
atomicFileStaging: true;
|
|
38
|
+
atomicStagingAncestry: true;
|
|
38
39
|
atomicFileMutation: true;
|
|
39
40
|
atomicEntryRemoval: true;
|
|
40
41
|
atomicTreeRemoval: true;
|
|
@@ -53,6 +54,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
53
54
|
constructor(options?: MemoryFileSystemOptions);
|
|
54
55
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
55
56
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
57
|
+
confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
56
58
|
private metadata;
|
|
57
59
|
private directory;
|
|
58
60
|
private addNode;
|
|
@@ -68,7 +68,7 @@ export class MemoryFileSystem {
|
|
|
68
68
|
permissions: true,
|
|
69
69
|
timestamps: true,
|
|
70
70
|
atomicRename: true,
|
|
71
|
-
atomicFileStaging: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicTreeRemoval: true,
|
|
71
|
+
atomicFileStaging: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicTreeRemoval: true,
|
|
72
72
|
atomicDirectoryMetadata: true,
|
|
73
73
|
streamingRead: true,
|
|
74
74
|
retainedRead: true,
|
|
@@ -117,6 +117,68 @@ export class MemoryFileSystem {
|
|
|
117
117
|
this.validatePath(path, "realpath");
|
|
118
118
|
return resolveMissingTarget(owner.root, path || ".", options.signal);
|
|
119
119
|
}
|
|
120
|
+
async confineExtraction(roots, options = {}) {
|
|
121
|
+
options.signal?.throwIfAborted();
|
|
122
|
+
const retained = new Map();
|
|
123
|
+
for (const root of roots) {
|
|
124
|
+
this.validatePath(root, "confineExtraction");
|
|
125
|
+
let path = "";
|
|
126
|
+
for (const component of root.split("/").filter(Boolean)) {
|
|
127
|
+
if (component === "." || component === "..")
|
|
128
|
+
this.fail("EINVAL", "confineExtraction", root);
|
|
129
|
+
path += `/${component}`;
|
|
130
|
+
const node = this.entry(path, "confineExtraction").node;
|
|
131
|
+
if (node.type !== "directory")
|
|
132
|
+
this.fail("ENOTDIR", "confineExtraction", path);
|
|
133
|
+
retained.set(path, node);
|
|
134
|
+
}
|
|
135
|
+
retained.set("/", this.root);
|
|
136
|
+
}
|
|
137
|
+
const allowed = new Set(["mkdir", "rm", "rmdir", "rename", "symlink", "link", "chmod", "utimes", "writeFile", "appendFile", "writeStream", "writeFileConditional", "removeFileConditional"]);
|
|
138
|
+
const reads = new Set(["access", "capabilitiesFor", "compareEntry", "lstat", "stat", "readFile", "readStream", "readdir", "readlink", "realpath"]);
|
|
139
|
+
const check = (path, followFinal) => {
|
|
140
|
+
if (!roots.some(root => root === "/" || path === root || path.startsWith(`${root}/`)))
|
|
141
|
+
this.fail("EPERM", "confineExtraction", path);
|
|
142
|
+
const components = path.split("/").filter(Boolean);
|
|
143
|
+
if (components.some(component => component === "." || component === ".."))
|
|
144
|
+
this.fail("EINVAL", "confineExtraction", path);
|
|
145
|
+
let current = "";
|
|
146
|
+
for (const [index, component] of components.entries()) {
|
|
147
|
+
current += `/${component}`;
|
|
148
|
+
const node = this.entry(current, "confineExtraction", true).node;
|
|
149
|
+
if (retained.has(current) && node !== retained.get(current))
|
|
150
|
+
this.fail("EAGAIN", "confineExtraction", current);
|
|
151
|
+
if (index < components.length - 1 && node?.type !== "directory")
|
|
152
|
+
this.fail("ENOTDIR", "confineExtraction", current);
|
|
153
|
+
if (followFinal && index === components.length - 1 && node?.type === "symlink")
|
|
154
|
+
this.fail("ELOOP", "confineExtraction", current);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
return new Proxy(this, {
|
|
158
|
+
get: (target, property) => {
|
|
159
|
+
if (property === "objects" || property === "confineExtraction")
|
|
160
|
+
return undefined;
|
|
161
|
+
const value = Reflect.get(target, property);
|
|
162
|
+
if (typeof value !== "function")
|
|
163
|
+
return value;
|
|
164
|
+
if (reads.has(String(property)))
|
|
165
|
+
return value.bind(target);
|
|
166
|
+
return (...args) => {
|
|
167
|
+
if (!allowed.has(String(property)))
|
|
168
|
+
throw new FsError("ENOTSUP", { syscall: String(property) });
|
|
169
|
+
// Stock mutations resolve and commit synchronously; writeStream retains
|
|
170
|
+
// its file node before its first await, so later chunks cannot redirect.
|
|
171
|
+
const paths = property === "symlink" ? [args[1]] : property === "link" || property === "rename" ? args.slice(0, 2) : [args[0]];
|
|
172
|
+
for (const path of paths) {
|
|
173
|
+
if (typeof path !== "string")
|
|
174
|
+
throw new FsError("EINVAL");
|
|
175
|
+
check(path, ["chmod", "utimes", "appendFile", "writeFile", "writeStream", "writeFileConditional", "link"].includes(String(property)));
|
|
176
|
+
}
|
|
177
|
+
return Reflect.apply(value, target, args);
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
120
182
|
metadata(mode) {
|
|
121
183
|
const now = Date.now();
|
|
122
184
|
return {
|
|
@@ -201,6 +263,10 @@ export class MemoryFileSystem {
|
|
|
201
263
|
}
|
|
202
264
|
resolve(path, syscall, options = {}) {
|
|
203
265
|
this.validatePath(path, syscall);
|
|
266
|
+
// Bound all component arrays allocated by this resolution, including cycles.
|
|
267
|
+
let remainingPathUnits = 65_536 - path.length;
|
|
268
|
+
if (remainingPathUnits < 0)
|
|
269
|
+
this.fail("ENAMETOOLONG", syscall, path);
|
|
204
270
|
const pending = path.split("/").filter(Boolean);
|
|
205
271
|
if (path.endsWith("/"))
|
|
206
272
|
pending.push("");
|
|
@@ -239,6 +305,9 @@ export class MemoryFileSystem {
|
|
|
239
305
|
if (node.type === "symlink" && (options.followFinal !== false || pending.length > 0)) {
|
|
240
306
|
if (++links > 40)
|
|
241
307
|
this.fail("ELOOP", syscall, path);
|
|
308
|
+
if (node.target.length > remainingPathUnits)
|
|
309
|
+
this.fail("ENAMETOOLONG", syscall, path);
|
|
310
|
+
remainingPathUnits -= node.target.length;
|
|
242
311
|
const target = node.target.split("/").filter(Boolean);
|
|
243
312
|
if (node.target.endsWith("/"))
|
|
244
313
|
target.push("");
|
|
@@ -278,6 +347,7 @@ export class MemoryFileSystem {
|
|
|
278
347
|
snapshot(node) {
|
|
279
348
|
return {
|
|
280
349
|
type: node.type,
|
|
350
|
+
filesystemType: "memory",
|
|
281
351
|
ioBlockSize: preferredIoBlockSize,
|
|
282
352
|
...(Number.isSafeInteger(node.revision) ? { revision: node.revision } : {}),
|
|
283
353
|
preferredIoBlockSize: 4096,
|
|
@@ -314,15 +384,17 @@ export class MemoryFileSystem {
|
|
|
314
384
|
}
|
|
315
385
|
open(path, options) {
|
|
316
386
|
return openFileDescriptor(path, options, {
|
|
317
|
-
positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
|
|
387
|
+
noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "volatile", position: true,
|
|
318
388
|
}, async (admitted) => {
|
|
319
389
|
const location = this.resolve(path, "open", {
|
|
320
|
-
followFinal: admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
|
|
390
|
+
followFinal: !admitted.noFollow && admitted.creation !== "exclusive", allowMissing: admitted.creation !== "never",
|
|
321
391
|
});
|
|
322
392
|
let node = location.node;
|
|
323
393
|
if (node) {
|
|
324
394
|
if (admitted.creation === "exclusive")
|
|
325
395
|
this.fail("EEXIST", "open", path);
|
|
396
|
+
if (node.type === "symlink" && admitted.noFollow)
|
|
397
|
+
this.fail("ELOOP", "open", path);
|
|
326
398
|
if (node.type !== "file")
|
|
327
399
|
this.fail("EISDIR", "open", path);
|
|
328
400
|
this.permission(node, admitted.access === "read" ? 4 : admitted.access === "write" ? 2 : 6, "open", path);
|
|
@@ -583,6 +655,25 @@ export class MemoryFileSystem {
|
|
|
583
655
|
}
|
|
584
656
|
async publishStagedFile(staging, destination, options) {
|
|
585
657
|
options.signal?.throwIfAborted();
|
|
658
|
+
if (options.ancestors) {
|
|
659
|
+
let path = "/";
|
|
660
|
+
const parentPath = destination.slice(0, destination.lastIndexOf("/")) || "/";
|
|
661
|
+
const paths = ["/", ...parentPath.split("/").filter(Boolean).map(component => {
|
|
662
|
+
path = path === "/" ? `/${component}` : `${path}/${component}`;
|
|
663
|
+
return path;
|
|
664
|
+
})];
|
|
665
|
+
if (paths.length !== options.ancestors.length)
|
|
666
|
+
this.fail("EINVAL", "publishStagedFile", destination);
|
|
667
|
+
for (let index = 0; index < paths.length; index++) {
|
|
668
|
+
const expected = options.ancestors[index];
|
|
669
|
+
if (expected.path !== paths[index])
|
|
670
|
+
this.fail("EINVAL", "publishStagedFile", destination);
|
|
671
|
+
const entry = this.entry(expected.path, "publishStagedFile");
|
|
672
|
+
if (entry.node?.type !== "directory")
|
|
673
|
+
this.fail("EAGAIN", "publishStagedFile", destination);
|
|
674
|
+
this.expectEntry(entry.node, expected.stat, expected.path, false);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
586
677
|
const { directory, file } = this.stagingLocations(staging);
|
|
587
678
|
this.expectEntry(file.node, staging.file.stat, staging.file.path);
|
|
588
679
|
const target = this.entry(destination, "publishStagedFile", true);
|
|
@@ -1212,5 +1303,5 @@ function stockRetainedResize(filesystem) {
|
|
|
1212
1303
|
return true;
|
|
1213
1304
|
}
|
|
1214
1305
|
export function createMemoryFileSystem(options = {}) {
|
|
1215
|
-
return new MemoryFileSystem(
|
|
1306
|
+
return new MemoryFileSystem(options);
|
|
1216
1307
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readConfigRecord } from "../../config.js";
|
|
2
2
|
export const defaultMemoryFileSystemLimits = Object.freeze({
|
|
3
|
-
maxFileBytes:
|
|
4
|
-
maxRetainedBytes:
|
|
5
|
-
maxMetadataUnits:
|
|
3
|
+
maxFileBytes: Infinity,
|
|
4
|
+
maxRetainedBytes: Infinity,
|
|
5
|
+
maxMetadataUnits: Infinity,
|
|
6
6
|
});
|
|
7
7
|
export function normalizeMemoryFileSystemLimits(options) {
|
|
8
8
|
const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
|
|
@@ -15,7 +15,9 @@ export function normalizeMemoryFileSystemLimits(options) {
|
|
|
15
15
|
limits.maxBytes = record.maxBytes;
|
|
16
16
|
}
|
|
17
17
|
for (const key of keys) {
|
|
18
|
-
|
|
18
|
+
if (!Object.hasOwn(record, key))
|
|
19
|
+
continue;
|
|
20
|
+
const value = record[key];
|
|
19
21
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
|
|
20
22
|
throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer`);
|
|
21
23
|
}
|
|
@@ -8,6 +8,9 @@ export interface MountFileSystemOptions {
|
|
|
8
8
|
export declare class MountFileSystem implements FileSystem {
|
|
9
9
|
readonly capabilities: FileSystemCapabilities;
|
|
10
10
|
private readonly mounts;
|
|
11
|
+
private activeNamespaceOperations;
|
|
12
|
+
private namespaceMutation;
|
|
13
|
+
private readonly namespaceWaiters;
|
|
11
14
|
constructor(options: MountFileSystemOptions);
|
|
12
15
|
private select;
|
|
13
16
|
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
@@ -16,6 +19,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
16
19
|
resizeFile(path: string, operation: FileResizeOperation, options?: FileResizeOptions): Promise<void>;
|
|
17
20
|
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
18
21
|
private error;
|
|
22
|
+
private acquireNamespace;
|
|
19
23
|
private operation;
|
|
20
24
|
private components;
|
|
21
25
|
private lookup;
|
|
@@ -22,9 +22,10 @@ function fail(code) {
|
|
|
22
22
|
throw new FsError(code);
|
|
23
23
|
}
|
|
24
24
|
function snapshotStat(stat) {
|
|
25
|
-
const { type, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
|
|
25
|
+
const { type, filesystemType, size, allocatedBytes, ioBlockSize, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, revision, identityScope, opaqueIdentity, opaqueVersion, ino, dev, rdevMajor, rdevMinor, nlink, uid, gid } = stat;
|
|
26
26
|
return {
|
|
27
27
|
type, size, mode, mtimeMs, atimeMs, ctimeMs,
|
|
28
|
+
...(filesystemType === undefined ? {} : { filesystemType }),
|
|
28
29
|
...(revision === undefined ? {} : { revision }),
|
|
29
30
|
...(allocatedBytes === undefined ? {} : { allocatedBytes }),
|
|
30
31
|
...(ioBlockSize === undefined ? {} : { ioBlockSize }),
|
|
@@ -45,6 +46,9 @@ function snapshotStat(stat) {
|
|
|
45
46
|
export class MountFileSystem {
|
|
46
47
|
capabilities;
|
|
47
48
|
mounts;
|
|
49
|
+
activeNamespaceOperations = 0;
|
|
50
|
+
namespaceMutation = false;
|
|
51
|
+
namespaceWaiters = new Set();
|
|
48
52
|
constructor(options) {
|
|
49
53
|
const mounts = [];
|
|
50
54
|
const paths = new Set();
|
|
@@ -98,6 +102,7 @@ export class MountFileSystem {
|
|
|
98
102
|
const common = (capability) => {
|
|
99
103
|
const optional = {
|
|
100
104
|
open: ["open"],
|
|
105
|
+
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
101
106
|
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
102
107
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
103
108
|
descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
@@ -116,7 +121,7 @@ export class MountFileSystem {
|
|
|
116
121
|
return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
|
|
117
122
|
};
|
|
118
123
|
const semantics = Object.fromEntries([
|
|
119
|
-
"atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
|
|
124
|
+
"trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
|
|
120
125
|
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
121
126
|
"remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
122
127
|
"streamingAppend", "randomAccessWrite", "descriptorWriteStream", "retainedResize", "atomicResize", "symlinks", "hardlinks", "permissions", "timestamps",
|
|
@@ -126,6 +131,7 @@ export class MountFileSystem {
|
|
|
126
131
|
readOnly: all("readOnly"),
|
|
127
132
|
...(append === undefined ? {} : { append }),
|
|
128
133
|
...semantics,
|
|
134
|
+
atomicStagingAncestry: false,
|
|
129
135
|
atomicRename: mounts.length === 1 && all("atomicRename"),
|
|
130
136
|
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
131
137
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
@@ -139,6 +145,7 @@ export class MountFileSystem {
|
|
|
139
145
|
return this.operation("capabilitiesFor", path, options, async () => {
|
|
140
146
|
const location = await this.resolve(path, options, {
|
|
141
147
|
allowMissing: options.create ?? true,
|
|
148
|
+
followFinal: options.create !== undefined || options.creation !== "exclusive",
|
|
142
149
|
...(options.create === undefined ? {} : { resizeCreate: options.create }),
|
|
143
150
|
});
|
|
144
151
|
const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
@@ -148,7 +155,7 @@ export class MountFileSystem {
|
|
|
148
155
|
? { ...observed, descriptorWriteStream: false } : observed;
|
|
149
156
|
const resize = declared.atomicResize === true && (typeof location.mount.backend.resizeFile !== "function" || declared.readOnly === true)
|
|
150
157
|
? { ...declared, atomicResize: false } : declared;
|
|
151
|
-
const withOpen = { ...resize, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
158
|
+
const withOpen = { ...resize, atomicStagingAncestry: false, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
152
159
|
const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
|
|
153
160
|
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
|
|
154
161
|
if (location.synthetic)
|
|
@@ -168,7 +175,9 @@ export class MountFileSystem {
|
|
|
168
175
|
return path === "/" || this.mounts.some((mount) => within(path, mount.path));
|
|
169
176
|
}
|
|
170
177
|
async openReadFile(path, options = {}) {
|
|
178
|
+
let release;
|
|
171
179
|
try {
|
|
180
|
+
release = await this.acquireNamespace(options);
|
|
172
181
|
options.signal?.throwIfAborted();
|
|
173
182
|
const location = await this.resolve(path, options);
|
|
174
183
|
if (location.synthetic)
|
|
@@ -178,6 +187,9 @@ export class MountFileSystem {
|
|
|
178
187
|
catch (error) {
|
|
179
188
|
throw error ? this.error(error, "openReadFile", path, options) : error;
|
|
180
189
|
}
|
|
190
|
+
finally {
|
|
191
|
+
release?.();
|
|
192
|
+
}
|
|
181
193
|
}
|
|
182
194
|
async resizeFile(path, operation, options = {}) {
|
|
183
195
|
return this.operation("resizeFile", path, options, async () => {
|
|
@@ -197,7 +209,9 @@ export class MountFileSystem {
|
|
|
197
209
|
});
|
|
198
210
|
}
|
|
199
211
|
async openResizeFile(path, options = {}) {
|
|
212
|
+
let release;
|
|
200
213
|
try {
|
|
214
|
+
release = await this.acquireNamespace(options);
|
|
201
215
|
options.signal?.throwIfAborted();
|
|
202
216
|
const resolution = this.resolve(path, options, { allowMissing: options.create === true, resizeCreate: options.create ?? false });
|
|
203
217
|
const signal = options.signal;
|
|
@@ -216,17 +230,56 @@ export class MountFileSystem {
|
|
|
216
230
|
catch (error) {
|
|
217
231
|
throw error ? this.error(error, "openResizeFile", path, options) : error;
|
|
218
232
|
}
|
|
233
|
+
finally {
|
|
234
|
+
release?.();
|
|
235
|
+
}
|
|
219
236
|
}
|
|
220
237
|
error(error, syscall, path, options, dest) {
|
|
221
|
-
if (options.signal?.aborted && error
|
|
238
|
+
if (options.signal?.aborted && Object.is(error, options.signal.reason))
|
|
222
239
|
return error;
|
|
223
240
|
return new FsError(toFsError(error).code, {
|
|
224
241
|
cause: error, syscall, path: globalPath(path), ...(dest === undefined ? {} : { dest: globalPath(dest) }),
|
|
225
242
|
});
|
|
226
243
|
}
|
|
244
|
+
// Keep guest namespace changes outside every path admission/action interval.
|
|
245
|
+
// Backend references held outside this view still require backend confinement.
|
|
246
|
+
async acquireNamespace(options, mutation = false) {
|
|
247
|
+
const signal = options.signal;
|
|
248
|
+
signal?.throwIfAborted();
|
|
249
|
+
while (this.namespaceMutation || mutation && this.activeNamespaceOperations > 0) {
|
|
250
|
+
await new Promise((resolve, reject) => {
|
|
251
|
+
const wake = () => { cleanup(); resolve(); };
|
|
252
|
+
const abort = () => { cleanup(); reject(signal.reason); };
|
|
253
|
+
const cleanup = () => {
|
|
254
|
+
this.namespaceWaiters.delete(wake);
|
|
255
|
+
signal?.removeEventListener("abort", abort);
|
|
256
|
+
};
|
|
257
|
+
this.namespaceWaiters.add(wake);
|
|
258
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
259
|
+
if (signal?.aborted)
|
|
260
|
+
abort();
|
|
261
|
+
});
|
|
262
|
+
signal?.throwIfAborted();
|
|
263
|
+
}
|
|
264
|
+
this.activeNamespaceOperations++;
|
|
265
|
+
if (mutation)
|
|
266
|
+
this.namespaceMutation = true;
|
|
267
|
+
return () => {
|
|
268
|
+
this.activeNamespaceOperations--;
|
|
269
|
+
if (mutation)
|
|
270
|
+
this.namespaceMutation = false;
|
|
271
|
+
for (const wake of this.namespaceWaiters)
|
|
272
|
+
wake();
|
|
273
|
+
};
|
|
274
|
+
}
|
|
227
275
|
async operation(syscall, path, options, action, dest, preserveReceipt = false) {
|
|
276
|
+
let release;
|
|
228
277
|
try {
|
|
229
|
-
|
|
278
|
+
release = await this.acquireNamespace(options, [
|
|
279
|
+
"rename", "symlink", "link", "unlink", "rm", "rmdir",
|
|
280
|
+
"removeEntryConditional", "removeFileConditional", "removeTreeConditional",
|
|
281
|
+
"publishStagedFile", "removeStagedFile",
|
|
282
|
+
].includes(syscall));
|
|
230
283
|
const result = await action();
|
|
231
284
|
if (!preserveReceipt)
|
|
232
285
|
options.signal?.throwIfAborted();
|
|
@@ -235,6 +288,9 @@ export class MountFileSystem {
|
|
|
235
288
|
catch (error) {
|
|
236
289
|
throw this.error(error, syscall, path, options, dest);
|
|
237
290
|
}
|
|
291
|
+
finally {
|
|
292
|
+
release?.();
|
|
293
|
+
}
|
|
238
294
|
}
|
|
239
295
|
components(path) {
|
|
240
296
|
validatePath(path);
|
|
@@ -423,18 +479,21 @@ export class MountFileSystem {
|
|
|
423
479
|
}
|
|
424
480
|
open(path, options) {
|
|
425
481
|
return openFileDescriptor(globalPath(path), options, {
|
|
426
|
-
positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
482
|
+
noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
427
483
|
}, async (admitted) => {
|
|
484
|
+
let release;
|
|
428
485
|
try {
|
|
486
|
+
release = await this.acquireNamespace(admitted);
|
|
429
487
|
const location = await this.resolve(path, admitted, {
|
|
430
|
-
allowMissing: admitted.creation !== "never", followFinal: admitted.creation !== "exclusive",
|
|
488
|
+
allowMissing: admitted.creation !== "never", followFinal: !admitted.noFollow && admitted.creation !== "exclusive",
|
|
431
489
|
});
|
|
432
490
|
if (location.synthetic)
|
|
433
491
|
fail("EISDIR");
|
|
434
492
|
if (admitted.access !== "read" || admitted.creation !== "never" || admitted.truncate || admitted.append)
|
|
435
493
|
this.mutable(location);
|
|
436
494
|
const backend = location.mount.backend;
|
|
437
|
-
const capabilities =
|
|
495
|
+
const capabilities = admitted.noFollow ? backend.capabilities
|
|
496
|
+
: await backend.capabilitiesFor?.(location.local, admitted) ?? backend.capabilities;
|
|
438
497
|
if (!backend.open || capabilities.open === false)
|
|
439
498
|
fail("ENOTSUP");
|
|
440
499
|
admitted.signal?.throwIfAborted();
|
|
@@ -451,6 +510,9 @@ export class MountFileSystem {
|
|
|
451
510
|
admitted.signal?.throwIfAborted();
|
|
452
511
|
throw this.error(error, "open", path, admitted);
|
|
453
512
|
}
|
|
513
|
+
finally {
|
|
514
|
+
release?.();
|
|
515
|
+
}
|
|
454
516
|
});
|
|
455
517
|
}
|
|
456
518
|
writeFile(path, data, options = {}) {
|
|
@@ -776,10 +838,7 @@ export class MountFileSystem {
|
|
|
776
838
|
}
|
|
777
839
|
}
|
|
778
840
|
else {
|
|
779
|
-
const
|
|
780
|
-
const data = await reader.readFile(origin.local, { ...options, maxBytes });
|
|
781
|
-
if (data.byteLength > maxBytes)
|
|
782
|
-
fail("EFBIG");
|
|
841
|
+
const data = await reader.readFile(origin.local, options);
|
|
783
842
|
options.signal?.throwIfAborted();
|
|
784
843
|
await writer.writeFile(target.local, data, writeOptions);
|
|
785
844
|
}
|
|
@@ -856,7 +915,9 @@ export class MountFileSystem {
|
|
|
856
915
|
});
|
|
857
916
|
}
|
|
858
917
|
async *readStream(path, options = {}) {
|
|
918
|
+
let release;
|
|
859
919
|
try {
|
|
920
|
+
release = await this.acquireNamespace(options);
|
|
860
921
|
options.signal?.throwIfAborted();
|
|
861
922
|
const location = await this.resolve(path, options);
|
|
862
923
|
if (location.synthetic)
|
|
@@ -871,6 +932,9 @@ export class MountFileSystem {
|
|
|
871
932
|
catch (error) {
|
|
872
933
|
throw this.error(error, "readStream", path, options);
|
|
873
934
|
}
|
|
935
|
+
finally {
|
|
936
|
+
release?.();
|
|
937
|
+
}
|
|
874
938
|
}
|
|
875
939
|
writeStream(path, source, options = {}) {
|
|
876
940
|
return this.operation("writeStream", path, options, async () => {
|
|
@@ -2,11 +2,15 @@ import type { FileStat, FileSystem, FsOptions, OpenFileOptions } from "../../con
|
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export interface ObjectFileVersion {
|
|
4
4
|
readonly revision: string;
|
|
5
|
+
/** Opaque lease/reservation token; independent of namespace revision. */
|
|
6
|
+
readonly publicationToken?: string;
|
|
5
7
|
readonly stat: FileStat;
|
|
6
8
|
read(position: number, maxBytes: number, options?: FsOptions): Promise<Uint8Array>;
|
|
7
9
|
close(): Promise<void>;
|
|
8
10
|
}
|
|
9
11
|
export interface ObjectFilePublicationOptions extends FsOptions {
|
|
12
|
+
/** Token from this descriptor's current retained version, if supplied by its store. */
|
|
13
|
+
readonly previousToken?: string;
|
|
10
14
|
readonly size: number;
|
|
11
15
|
readonly mode: number;
|
|
12
16
|
}
|
|
@@ -5,12 +5,12 @@ import { validatePath } from "../../contracts/virtual-path.js";
|
|
|
5
5
|
import { openFileDescriptor } from "../descriptor.js";
|
|
6
6
|
export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
7
7
|
const chunkBytes = options.chunkBytes ?? 65536;
|
|
8
|
-
const maxStagedBytes = options.maxStagedBytes ??
|
|
9
|
-
const maxStagedPages = options.maxStagedPages ??
|
|
10
|
-
const maxFileBytes = options.maxFileBytes ??
|
|
11
|
-
const maxOpenFiles = options.maxOpenFiles ??
|
|
12
|
-
if (![chunkBytes, maxStagedBytes, maxStagedPages, maxFileBytes, maxOpenFiles].every(value => Number.isSafeInteger(value) && value > 0)
|
|
13
|
-
||
|
|
8
|
+
const maxStagedBytes = options.maxStagedBytes ?? Infinity;
|
|
9
|
+
const maxStagedPages = options.maxStagedPages ?? Infinity;
|
|
10
|
+
const maxFileBytes = options.maxFileBytes ?? Infinity;
|
|
11
|
+
const maxOpenFiles = options.maxOpenFiles ?? Infinity;
|
|
12
|
+
if (![chunkBytes, options.maxStagedBytes, options.maxStagedPages, options.maxFileBytes, options.maxOpenFiles].every(value => value === undefined || Number.isSafeInteger(value) && value > 0)
|
|
13
|
+
|| typeof store.acquire !== "function"
|
|
14
14
|
|| store.publish !== undefined && typeof store.publish !== "function"
|
|
15
15
|
|| store.createStaging !== undefined && typeof store.createStaging !== "function")
|
|
16
16
|
throw new TypeError("Invalid object descriptor configuration");
|
|
@@ -50,6 +50,7 @@ export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
|
50
50
|
};
|
|
51
51
|
const version = (value) => {
|
|
52
52
|
if (!value || typeof value.revision !== "string" || value.revision.length === 0 || value.revision.length > 4096
|
|
53
|
+
|| value.publicationToken !== undefined && (typeof value.publicationToken !== "string" || value.publicationToken.length === 0 || value.publicationToken.length > 4096)
|
|
53
54
|
|| !value.stat || value.stat.type !== "file" || !Number.isSafeInteger(value.stat.size) || value.stat.size < 0
|
|
54
55
|
|| !Number.isSafeInteger(value.stat.mode) || value.stat.mode < 0 || value.stat.mode > 0o177777
|
|
55
56
|
|| ![value.stat.mtimeMs, value.stat.atimeMs, value.stat.ctimeMs].every(Number.isFinite)
|
|
@@ -57,7 +58,7 @@ export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
|
57
58
|
throw new FsError("EIO", { message: "Invalid immutable object version" });
|
|
58
59
|
if (value.stat.size > maxFileBytes)
|
|
59
60
|
throw new FsError("EFBIG", { message: "Object descriptor file limit exceeded" });
|
|
60
|
-
return Object.freeze({ revision: value.revision, stat: Object.freeze({ ...value.stat }), read: value.read.bind(value), close: value.close.bind(value) });
|
|
61
|
+
return Object.freeze({ revision: value.revision, ...(value.publicationToken === undefined ? {} : { publicationToken: value.publicationToken }), stat: Object.freeze({ ...value.stat }), read: value.read.bind(value), close: value.close.bind(value) });
|
|
61
62
|
};
|
|
62
63
|
const clearPages = (state) => {
|
|
63
64
|
stagedBytes -= state.pages.size * chunkBytes;
|
|
@@ -181,7 +182,7 @@ export function withObjectFileDescriptors(filesystem, store, options = {}) {
|
|
|
181
182
|
yield bytes;
|
|
182
183
|
}
|
|
183
184
|
})();
|
|
184
|
-
received = await store.publish(path, expected, source, { ...selected, size: state.size, mode: (state.head?.stat.mode ?? admitted.mode) & 0o7777 });
|
|
185
|
+
received = await store.publish(path, expected, source, { ...selected, ...(state.head?.publicationToken === undefined ? {} : { previousToken: state.head.publicationToken }), size: state.size, mode: (state.head?.stat.mode ?? admitted.mode) & 0o7777 });
|
|
185
186
|
});
|
|
186
187
|
const published = version(received);
|
|
187
188
|
if (emitted !== state.size || published.stat.size !== state.size || published.revision === expected)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ByteSource } from "../../contracts/io.js";
|
|
2
2
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
3
|
-
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
4
4
|
export interface OverlayFileSystemOptions {
|
|
5
5
|
readonly upper: FileSystem;
|
|
6
6
|
readonly lower: FileSystem;
|
|
@@ -21,7 +21,9 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
21
21
|
open(path: string, options: OpenFileOptions): Promise<FileDescriptor>;
|
|
22
22
|
private run;
|
|
23
23
|
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
24
|
-
openReadFile(path: string, options?: OpenReadFileOptions): Promise<
|
|
24
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
25
|
+
private pinRead;
|
|
26
|
+
private handleBytes;
|
|
25
27
|
private writable;
|
|
26
28
|
private permission;
|
|
27
29
|
private maybeStat;
|