@poe-platform/safe-fs 0.1.720 → 0.1.722
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 +64 -18
- package/dist/safe-fs/bridge/filesystem.d.ts +2 -0
- package/dist/safe-fs/bridge/filesystem.js +17 -7
- package/dist/safe-fs/bridge/types.d.ts +2 -0
- package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +12 -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 +1 -0
- package/dist/safe-fs/fs/memory/index.js +75 -3
- 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 +74 -11
- package/dist/safe-fs/fs/object-publication/index.js +6 -6
- 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 +18 -16
- 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 +58 -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/namespace.js +12 -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 +30 -22
- package/dist/safe-fs/node/filesystem.d.ts +4 -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
|
@@ -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);
|
|
@@ -53,6 +53,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
53
53
|
constructor(options?: MemoryFileSystemOptions);
|
|
54
54
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
55
55
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
56
|
+
confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
56
57
|
private metadata;
|
|
57
58
|
private directory;
|
|
58
59
|
private addNode;
|
|
@@ -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"]);
|
|
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", "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);
|
|
@@ -1212,5 +1284,5 @@ function stockRetainedResize(filesystem) {
|
|
|
1212
1284
|
return true;
|
|
1213
1285
|
}
|
|
1214
1286
|
export function createMemoryFileSystem(options = {}) {
|
|
1215
|
-
return new MemoryFileSystem(
|
|
1287
|
+
return new MemoryFileSystem(options);
|
|
1216
1288
|
}
|
|
@@ -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",
|
|
@@ -139,6 +144,7 @@ export class MountFileSystem {
|
|
|
139
144
|
return this.operation("capabilitiesFor", path, options, async () => {
|
|
140
145
|
const location = await this.resolve(path, options, {
|
|
141
146
|
allowMissing: options.create ?? true,
|
|
147
|
+
followFinal: options.create !== undefined || options.creation !== "exclusive",
|
|
142
148
|
...(options.create === undefined ? {} : { resizeCreate: options.create }),
|
|
143
149
|
});
|
|
144
150
|
const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
@@ -168,7 +174,9 @@ export class MountFileSystem {
|
|
|
168
174
|
return path === "/" || this.mounts.some((mount) => within(path, mount.path));
|
|
169
175
|
}
|
|
170
176
|
async openReadFile(path, options = {}) {
|
|
177
|
+
let release;
|
|
171
178
|
try {
|
|
179
|
+
release = await this.acquireNamespace(options);
|
|
172
180
|
options.signal?.throwIfAborted();
|
|
173
181
|
const location = await this.resolve(path, options);
|
|
174
182
|
if (location.synthetic)
|
|
@@ -178,6 +186,9 @@ export class MountFileSystem {
|
|
|
178
186
|
catch (error) {
|
|
179
187
|
throw error ? this.error(error, "openReadFile", path, options) : error;
|
|
180
188
|
}
|
|
189
|
+
finally {
|
|
190
|
+
release?.();
|
|
191
|
+
}
|
|
181
192
|
}
|
|
182
193
|
async resizeFile(path, operation, options = {}) {
|
|
183
194
|
return this.operation("resizeFile", path, options, async () => {
|
|
@@ -197,7 +208,9 @@ export class MountFileSystem {
|
|
|
197
208
|
});
|
|
198
209
|
}
|
|
199
210
|
async openResizeFile(path, options = {}) {
|
|
211
|
+
let release;
|
|
200
212
|
try {
|
|
213
|
+
release = await this.acquireNamespace(options);
|
|
201
214
|
options.signal?.throwIfAborted();
|
|
202
215
|
const resolution = this.resolve(path, options, { allowMissing: options.create === true, resizeCreate: options.create ?? false });
|
|
203
216
|
const signal = options.signal;
|
|
@@ -216,17 +229,56 @@ export class MountFileSystem {
|
|
|
216
229
|
catch (error) {
|
|
217
230
|
throw error ? this.error(error, "openResizeFile", path, options) : error;
|
|
218
231
|
}
|
|
232
|
+
finally {
|
|
233
|
+
release?.();
|
|
234
|
+
}
|
|
219
235
|
}
|
|
220
236
|
error(error, syscall, path, options, dest) {
|
|
221
|
-
if (options.signal?.aborted && error
|
|
237
|
+
if (options.signal?.aborted && Object.is(error, options.signal.reason))
|
|
222
238
|
return error;
|
|
223
239
|
return new FsError(toFsError(error).code, {
|
|
224
240
|
cause: error, syscall, path: globalPath(path), ...(dest === undefined ? {} : { dest: globalPath(dest) }),
|
|
225
241
|
});
|
|
226
242
|
}
|
|
243
|
+
// Keep guest namespace changes outside every path admission/action interval.
|
|
244
|
+
// Backend references held outside this view still require backend confinement.
|
|
245
|
+
async acquireNamespace(options, mutation = false) {
|
|
246
|
+
const signal = options.signal;
|
|
247
|
+
signal?.throwIfAborted();
|
|
248
|
+
while (this.namespaceMutation || mutation && this.activeNamespaceOperations > 0) {
|
|
249
|
+
await new Promise((resolve, reject) => {
|
|
250
|
+
const wake = () => { cleanup(); resolve(); };
|
|
251
|
+
const abort = () => { cleanup(); reject(signal.reason); };
|
|
252
|
+
const cleanup = () => {
|
|
253
|
+
this.namespaceWaiters.delete(wake);
|
|
254
|
+
signal?.removeEventListener("abort", abort);
|
|
255
|
+
};
|
|
256
|
+
this.namespaceWaiters.add(wake);
|
|
257
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
258
|
+
if (signal?.aborted)
|
|
259
|
+
abort();
|
|
260
|
+
});
|
|
261
|
+
signal?.throwIfAborted();
|
|
262
|
+
}
|
|
263
|
+
this.activeNamespaceOperations++;
|
|
264
|
+
if (mutation)
|
|
265
|
+
this.namespaceMutation = true;
|
|
266
|
+
return () => {
|
|
267
|
+
this.activeNamespaceOperations--;
|
|
268
|
+
if (mutation)
|
|
269
|
+
this.namespaceMutation = false;
|
|
270
|
+
for (const wake of this.namespaceWaiters)
|
|
271
|
+
wake();
|
|
272
|
+
};
|
|
273
|
+
}
|
|
227
274
|
async operation(syscall, path, options, action, dest, preserveReceipt = false) {
|
|
275
|
+
let release;
|
|
228
276
|
try {
|
|
229
|
-
|
|
277
|
+
release = await this.acquireNamespace(options, [
|
|
278
|
+
"rename", "symlink", "link", "unlink", "rm", "rmdir",
|
|
279
|
+
"removeEntryConditional", "removeFileConditional", "removeTreeConditional",
|
|
280
|
+
"publishStagedFile", "removeStagedFile",
|
|
281
|
+
].includes(syscall));
|
|
230
282
|
const result = await action();
|
|
231
283
|
if (!preserveReceipt)
|
|
232
284
|
options.signal?.throwIfAborted();
|
|
@@ -235,6 +287,9 @@ export class MountFileSystem {
|
|
|
235
287
|
catch (error) {
|
|
236
288
|
throw this.error(error, syscall, path, options, dest);
|
|
237
289
|
}
|
|
290
|
+
finally {
|
|
291
|
+
release?.();
|
|
292
|
+
}
|
|
238
293
|
}
|
|
239
294
|
components(path) {
|
|
240
295
|
validatePath(path);
|
|
@@ -423,18 +478,21 @@ export class MountFileSystem {
|
|
|
423
478
|
}
|
|
424
479
|
open(path, options) {
|
|
425
480
|
return openFileDescriptor(globalPath(path), options, {
|
|
426
|
-
positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
481
|
+
noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
|
|
427
482
|
}, async (admitted) => {
|
|
483
|
+
let release;
|
|
428
484
|
try {
|
|
485
|
+
release = await this.acquireNamespace(admitted);
|
|
429
486
|
const location = await this.resolve(path, admitted, {
|
|
430
|
-
allowMissing: admitted.creation !== "never", followFinal: admitted.creation !== "exclusive",
|
|
487
|
+
allowMissing: admitted.creation !== "never", followFinal: !admitted.noFollow && admitted.creation !== "exclusive",
|
|
431
488
|
});
|
|
432
489
|
if (location.synthetic)
|
|
433
490
|
fail("EISDIR");
|
|
434
491
|
if (admitted.access !== "read" || admitted.creation !== "never" || admitted.truncate || admitted.append)
|
|
435
492
|
this.mutable(location);
|
|
436
493
|
const backend = location.mount.backend;
|
|
437
|
-
const capabilities =
|
|
494
|
+
const capabilities = admitted.noFollow ? backend.capabilities
|
|
495
|
+
: await backend.capabilitiesFor?.(location.local, admitted) ?? backend.capabilities;
|
|
438
496
|
if (!backend.open || capabilities.open === false)
|
|
439
497
|
fail("ENOTSUP");
|
|
440
498
|
admitted.signal?.throwIfAborted();
|
|
@@ -451,6 +509,9 @@ export class MountFileSystem {
|
|
|
451
509
|
admitted.signal?.throwIfAborted();
|
|
452
510
|
throw this.error(error, "open", path, admitted);
|
|
453
511
|
}
|
|
512
|
+
finally {
|
|
513
|
+
release?.();
|
|
514
|
+
}
|
|
454
515
|
});
|
|
455
516
|
}
|
|
456
517
|
writeFile(path, data, options = {}) {
|
|
@@ -776,10 +837,7 @@ export class MountFileSystem {
|
|
|
776
837
|
}
|
|
777
838
|
}
|
|
778
839
|
else {
|
|
779
|
-
const
|
|
780
|
-
const data = await reader.readFile(origin.local, { ...options, maxBytes });
|
|
781
|
-
if (data.byteLength > maxBytes)
|
|
782
|
-
fail("EFBIG");
|
|
840
|
+
const data = await reader.readFile(origin.local, options);
|
|
783
841
|
options.signal?.throwIfAborted();
|
|
784
842
|
await writer.writeFile(target.local, data, writeOptions);
|
|
785
843
|
}
|
|
@@ -856,7 +914,9 @@ export class MountFileSystem {
|
|
|
856
914
|
});
|
|
857
915
|
}
|
|
858
916
|
async *readStream(path, options = {}) {
|
|
917
|
+
let release;
|
|
859
918
|
try {
|
|
919
|
+
release = await this.acquireNamespace(options);
|
|
860
920
|
options.signal?.throwIfAborted();
|
|
861
921
|
const location = await this.resolve(path, options);
|
|
862
922
|
if (location.synthetic)
|
|
@@ -871,6 +931,9 @@ export class MountFileSystem {
|
|
|
871
931
|
catch (error) {
|
|
872
932
|
throw this.error(error, "readStream", path, options);
|
|
873
933
|
}
|
|
934
|
+
finally {
|
|
935
|
+
release?.();
|
|
936
|
+
}
|
|
874
937
|
}
|
|
875
938
|
writeStream(path, source, options = {}) {
|
|
876
939
|
return this.operation("writeStream", path, options, async () => {
|
|
@@ -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");
|
|
@@ -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;
|