@poe-platform/safe-fs 0.1.512 → 0.1.514
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/dist/safe-fs/bridge/stats.js +1 -1
- package/dist/safe-fs/contracts/errors.d.ts +1 -0
- package/dist/safe-fs/contracts/errors.js +1 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +22 -2
- package/dist/safe-fs/fs/capabilities.d.ts +4 -2
- package/dist/safe-fs/fs/capabilities.js +80 -9
- package/dist/safe-fs/fs/devices/index.d.ts +4 -3
- package/dist/safe-fs/fs/devices/index.js +75 -16
- package/dist/safe-fs/fs/devices/path.d.ts +1 -1
- package/dist/safe-fs/fs/devices/path.js +111 -7
- package/dist/safe-fs/fs/memory/index.d.ts +5 -2
- package/dist/safe-fs/fs/memory/index.js +107 -10
- package/dist/safe-fs/fs/mount/index.d.ts +4 -3
- package/dist/safe-fs/fs/mount/index.js +66 -14
- package/dist/safe-fs/fs/overlay/index.d.ts +3 -3
- package/dist/safe-fs/fs/overlay/index.js +9 -3
- package/dist/safe-fs/fs/path-namespace.d.ts +4 -0
- package/dist/safe-fs/fs/path-namespace.js +63 -0
- package/dist/safe-fs/fs/quota/index.js +142 -6
- package/dist/safe-fs/fs/readonly/index.d.ts +4 -3
- package/dist/safe-fs/fs/readonly/index.js +14 -1
- package/dist/safe-fs/fs/real/index.d.ts +3 -2
- package/dist/safe-fs/fs/real/index.js +177 -8
- package/dist/safe-fs/fs/scoped.js +111 -13
- package/dist/safe-fs/native/fs-seek/linux-x64-glibc.node +0 -0
- package/dist/safe-fs/native/fs-seek/loader.d.ts +5 -0
- package/dist/safe-fs/native/fs-seek/loader.mjs +120 -0
- package/dist/safe-fs/native/fs-seek/manifest.json +46 -0
- package/dist/safe-fs/node/native-seek.d.ts +3 -0
- package/dist/safe-fs/node/native-seek.js +54 -0
- package/package.json +7 -1
|
@@ -27,6 +27,7 @@ declare const descriptions: {
|
|
|
27
27
|
readonly EPERM: "operation not permitted";
|
|
28
28
|
readonly EPIPE: "broken pipe";
|
|
29
29
|
readonly EROFS: "read-only file system";
|
|
30
|
+
readonly ESPIPE: "invalid seek";
|
|
30
31
|
readonly ETIMEDOUT: "operation timed out";
|
|
31
32
|
readonly EXDEV: "cross-device link not permitted";
|
|
32
33
|
};
|
|
@@ -5,6 +5,7 @@ export interface FileStat {
|
|
|
5
5
|
readonly type: FileType;
|
|
6
6
|
readonly size: number;
|
|
7
7
|
readonly allocatedBytes?: number;
|
|
8
|
+
readonly preferredIoBlockSize?: number;
|
|
8
9
|
readonly mode: number;
|
|
9
10
|
readonly mtimeMs: number;
|
|
10
11
|
readonly atimeMs: number;
|
|
@@ -54,6 +55,7 @@ export interface FileSystemCapabilities {
|
|
|
54
55
|
readonly snapshotRmdir?: boolean;
|
|
55
56
|
readonly streamingRead?: boolean;
|
|
56
57
|
readonly retainedRead?: boolean;
|
|
58
|
+
readonly retainedResize?: boolean;
|
|
57
59
|
readonly streamingWrite?: boolean;
|
|
58
60
|
readonly descriptorWriteStream?: boolean;
|
|
59
61
|
readonly [capability: string]: boolean | undefined;
|
|
@@ -61,9 +63,26 @@ export interface FileSystemCapabilities {
|
|
|
61
63
|
export interface FsOptions {
|
|
62
64
|
readonly signal?: AbortSignal;
|
|
63
65
|
}
|
|
66
|
+
export interface OpenReadFileOptions extends FsOptions {
|
|
67
|
+
readonly allowDirectory?: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface CapabilityQueryOptions extends OpenReadFileOptions {
|
|
70
|
+
readonly create?: boolean;
|
|
71
|
+
}
|
|
64
72
|
export interface FileReadHandle {
|
|
65
73
|
stat(options?: FsOptions): Promise<FileStat>;
|
|
66
74
|
read(position: number, maxBytes: number, options?: FsOptions): Promise<Uint8Array>;
|
|
75
|
+
seekEnd?: ((options?: FsOptions) => Promise<bigint>) | undefined;
|
|
76
|
+
close(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export interface OpenResizeFileOptions extends FsOptions {
|
|
79
|
+
readonly create?: boolean;
|
|
80
|
+
readonly mode?: number;
|
|
81
|
+
}
|
|
82
|
+
export interface FileResizeHandle {
|
|
83
|
+
stat(options?: FsOptions): Promise<FileStat>;
|
|
84
|
+
truncate(length: number, options?: FsOptions): Promise<void>;
|
|
85
|
+
seekEnd?: ((options?: FsOptions) => Promise<bigint>) | undefined;
|
|
67
86
|
close(): Promise<void>;
|
|
68
87
|
}
|
|
69
88
|
export interface RenameOptions extends FsOptions {
|
|
@@ -107,9 +126,10 @@ export interface ReadStreamOptions extends FsOptions {
|
|
|
107
126
|
}
|
|
108
127
|
export interface FileSystem {
|
|
109
128
|
readonly capabilities: FileSystemCapabilities;
|
|
110
|
-
openReadFile?(path: string, options?:
|
|
129
|
+
openReadFile?(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
130
|
+
openResizeFile?(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
111
131
|
canonicalizeMissingTarget?(path: string, options?: FsOptions): string | undefined;
|
|
112
|
-
capabilitiesFor?(path: string, options?:
|
|
132
|
+
capabilitiesFor?(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
113
133
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
114
134
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
115
135
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { FileReadHandle, FileSystem, FileSystemCapabilities,
|
|
1
|
+
import type { FileReadHandle, FileResizeHandle, FileSystem, FileSystemCapabilities, OpenReadFileOptions, OpenResizeFileOptions } from "../contracts/filesystem.js";
|
|
2
2
|
export declare function retainedReadCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
|
|
3
|
-
export declare function openRetainedReadFile(filesystem: FileSystem, path: string, options:
|
|
3
|
+
export declare function openRetainedReadFile(filesystem: FileSystem, path: string, options: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
4
4
|
export declare function requireCapabilities(...values: readonly (boolean | undefined)[]): boolean | undefined;
|
|
5
|
+
export declare function retainedResizeCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
|
|
6
|
+
export declare function openRetainedResizeFile(filesystem: FileSystem, path: string, options: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
5
7
|
export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
6
8
|
export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
@@ -4,29 +4,100 @@ export function retainedReadCapabilities(filesystem, capabilities = filesystem.c
|
|
|
4
4
|
return typeof filesystem.openReadFile === "function" ? capabilities : { ...capabilities, retainedRead: false };
|
|
5
5
|
}
|
|
6
6
|
export async function openRetainedReadFile(filesystem, path, options) {
|
|
7
|
+
const signal = options.signal;
|
|
7
8
|
let handle;
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
const available = typeof filesystem.openReadFile === "function";
|
|
12
|
+
signal?.throwIfAborted();
|
|
13
|
+
if (!available)
|
|
11
14
|
throw new FsError("ENOTSUP", { syscall: "openReadFile", path });
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const query = filesystem.capabilitiesFor;
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
const metadata = Promise.resolve(query == null ? undefined : Reflect.apply(query, filesystem, [path, options]));
|
|
18
|
+
const queried = await (signal ? new Promise((resolve, reject) => {
|
|
19
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
20
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
21
|
+
metadata.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
22
|
+
if (signal.aborted)
|
|
23
|
+
abort();
|
|
24
|
+
}) : metadata);
|
|
25
|
+
signal?.throwIfAborted();
|
|
26
|
+
const capabilities = queried ?? filesystem.capabilities;
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
const supported = capabilities.retainedRead;
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
if (supported !== true)
|
|
15
31
|
throw new FsError("ENOTSUP", { syscall: "openReadFile", path });
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
const acquire = filesystem.openReadFile;
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
if (typeof acquire !== "function")
|
|
35
|
+
throw new FsError("ENOTSUP", { syscall: "openReadFile", path });
|
|
36
|
+
handle = await Reflect.apply(acquire, filesystem, [path, options]);
|
|
37
|
+
signal?.throwIfAborted();
|
|
18
38
|
return handle;
|
|
19
39
|
}
|
|
20
40
|
catch (error) {
|
|
21
41
|
if (handle)
|
|
22
42
|
await finishCleanup(() => handle.close(), true);
|
|
23
|
-
|
|
43
|
+
signal?.throwIfAborted();
|
|
24
44
|
throw error;
|
|
25
45
|
}
|
|
26
46
|
}
|
|
27
47
|
export function requireCapabilities(...values) {
|
|
28
48
|
return values.some(value => value === false) ? false : values.every(value => value === true) ? true : undefined;
|
|
29
49
|
}
|
|
50
|
+
export function retainedResizeCapabilities(filesystem, capabilities = filesystem.capabilities) {
|
|
51
|
+
if (capabilities.retainedResize !== true)
|
|
52
|
+
return capabilities;
|
|
53
|
+
return typeof filesystem.openResizeFile === "function" && capabilities.readOnly !== true
|
|
54
|
+
? capabilities : { ...capabilities, retainedResize: false };
|
|
55
|
+
}
|
|
56
|
+
export async function openRetainedResizeFile(filesystem, path, options) {
|
|
57
|
+
const signal = options.signal;
|
|
58
|
+
let handle;
|
|
59
|
+
try {
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
const available = typeof filesystem.openResizeFile === "function";
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
if (!available)
|
|
64
|
+
throw new FsError("ENOTSUP", { syscall: "openResizeFile", path });
|
|
65
|
+
const query = filesystem.capabilitiesFor;
|
|
66
|
+
signal?.throwIfAborted();
|
|
67
|
+
const intent = { ...(signal === undefined ? {} : { signal }), create: options.create === true };
|
|
68
|
+
signal?.throwIfAborted();
|
|
69
|
+
const metadata = Promise.resolve(query === undefined ? filesystem.capabilities : Reflect.apply(query, filesystem, [path, intent]));
|
|
70
|
+
const capabilities = await (signal ? new Promise((resolve, reject) => {
|
|
71
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
72
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
73
|
+
metadata.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
74
|
+
if (signal.aborted)
|
|
75
|
+
abort();
|
|
76
|
+
}) : metadata);
|
|
77
|
+
signal?.throwIfAborted();
|
|
78
|
+
const readOnly = capabilities.readOnly;
|
|
79
|
+
signal?.throwIfAborted();
|
|
80
|
+
if (readOnly === true)
|
|
81
|
+
throw new FsError("EROFS", { syscall: "openResizeFile", path });
|
|
82
|
+
const supported = capabilities.retainedResize;
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
if (supported !== true)
|
|
85
|
+
throw new FsError("ENOTSUP", { syscall: "openResizeFile", path });
|
|
86
|
+
const acquire = filesystem.openResizeFile;
|
|
87
|
+
signal?.throwIfAborted();
|
|
88
|
+
if (typeof acquire !== "function")
|
|
89
|
+
throw new FsError("ENOTSUP", { syscall: "openResizeFile", path });
|
|
90
|
+
handle = await Reflect.apply(acquire, filesystem, [path, options]);
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
return handle;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (handle)
|
|
96
|
+
await finishCleanup(() => handle.close(), true);
|
|
97
|
+
signal?.throwIfAborted();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
30
101
|
export function readOnlyCapabilities(capabilities) {
|
|
31
102
|
const inspection = Object.fromEntries([
|
|
32
103
|
"read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories",
|
|
@@ -37,7 +108,7 @@ export function readOnlyCapabilities(capabilities) {
|
|
|
37
108
|
mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
|
|
38
109
|
rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
|
|
39
110
|
randomAccessWrite: false, hardlinks: false, permissions: false, timestamps: false,
|
|
40
|
-
descriptorWriteStream: false,
|
|
111
|
+
descriptorWriteStream: false, retainedResize: false,
|
|
41
112
|
atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
|
|
42
113
|
});
|
|
43
114
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export declare class DeviceFileSystem implements FileSystem {
|
|
4
4
|
#private;
|
|
5
5
|
readonly capabilities: FileSystemCapabilities;
|
|
6
6
|
constructor(filesystem: FileSystem);
|
|
7
7
|
canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
|
|
8
|
-
capabilitiesFor(path: string, options?:
|
|
8
|
+
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
9
9
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
10
10
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
11
11
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
|
|
12
12
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
13
13
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
14
14
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
15
|
-
openReadFile(path: string, options?:
|
|
15
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
16
|
+
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
16
17
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
17
18
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
18
19
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
@@ -3,6 +3,8 @@ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admissi
|
|
|
3
3
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
4
4
|
import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
|
|
5
5
|
import { deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
|
|
6
|
+
import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities } from "../capabilities.js";
|
|
7
|
+
import { pathNamespace } from "../path-namespace.js";
|
|
6
8
|
const views = new WeakMap();
|
|
7
9
|
const deviceCapabilities = Object.freeze({
|
|
8
10
|
readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
|
|
@@ -11,13 +13,13 @@ const deviceCapabilities = Object.freeze({
|
|
|
11
13
|
remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
|
|
12
14
|
mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
|
|
13
15
|
permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
|
|
14
|
-
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true,
|
|
16
|
+
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true,
|
|
15
17
|
});
|
|
16
18
|
function globalCapabilities(filesystem) {
|
|
17
19
|
const capabilities = { readOnly: false };
|
|
18
20
|
const optional = {
|
|
19
21
|
streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
|
|
20
|
-
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"],
|
|
22
|
+
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"],
|
|
21
23
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"],
|
|
22
24
|
timestamps: ["utimes"], readlink: ["readlink"], truncate: ["truncate"], removeDirectory: ["rmdir"],
|
|
23
25
|
};
|
|
@@ -30,6 +32,8 @@ function globalCapabilities(filesystem) {
|
|
|
30
32
|
let declared = filesystem.capabilities[key];
|
|
31
33
|
if (declared === true && optional[key]?.some(method => typeof filesystem[method] !== "function"))
|
|
32
34
|
declared = false;
|
|
35
|
+
if (key === "retainedResize" && filesystem.capabilities.readOnly === true)
|
|
36
|
+
declared = false;
|
|
33
37
|
if (key === "descriptorWriteStream" && filesystem.capabilities.streamingWrite === false)
|
|
34
38
|
declared = false;
|
|
35
39
|
return declared === deviceCapabilities[key] ? declared : undefined;
|
|
@@ -49,11 +53,13 @@ export class DeviceFileSystem {
|
|
|
49
53
|
#directoryStat;
|
|
50
54
|
constructor(filesystem) {
|
|
51
55
|
this.#filesystem = filesystem;
|
|
56
|
+
Object.defineProperty(this, pathNamespace, { get: () => Reflect.get(filesystem, pathNamespace) });
|
|
52
57
|
this.capabilities = globalCapabilities(filesystem);
|
|
53
58
|
const identityScope = Object.freeze({});
|
|
54
|
-
|
|
55
|
-
mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1 }
|
|
56
|
-
this.#
|
|
59
|
+
const stat = { mode: 0o020666, size: 0, allocatedBytes: 0,
|
|
60
|
+
mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1 };
|
|
61
|
+
this.#nullStat = Object.freeze({ ...stat, type: "character", preferredIoBlockSize: 4096 });
|
|
62
|
+
this.#directoryStat = Object.freeze({ ...stat, type: "directory", mode: 0o040755, ino: 2 });
|
|
57
63
|
const methods = new Map();
|
|
58
64
|
const view = new Proxy(this, {
|
|
59
65
|
set: (_target, property, value) => Reflect.set(filesystem, property, value, filesystem),
|
|
@@ -80,8 +86,8 @@ export class DeviceFileSystem {
|
|
|
80
86
|
});
|
|
81
87
|
return view;
|
|
82
88
|
}
|
|
83
|
-
#resolve(path, options, followFinal = true) {
|
|
84
|
-
return resolveDevicePath(this.#filesystem, path, options, followFinal);
|
|
89
|
+
#resolve(path, options, followFinal = true, resizeCreate) {
|
|
90
|
+
return resolveDevicePath(this.#filesystem, path, options, followFinal, resizeCreate);
|
|
85
91
|
}
|
|
86
92
|
async #mutable(path, options, followFinal = true) {
|
|
87
93
|
if (reserved(await this.#resolve(path, options, followFinal)))
|
|
@@ -106,14 +112,23 @@ export class DeviceFileSystem {
|
|
|
106
112
|
return reserved(resolved) ? resolved : this.#filesystem.canonicalizeMissingTarget?.(path, options);
|
|
107
113
|
}
|
|
108
114
|
async capabilitiesFor(path, options = {}) {
|
|
109
|
-
const resolved = await this.#resolve(path, options);
|
|
115
|
+
const resolved = await this.#resolve(path, options, true, options.create);
|
|
116
|
+
options.signal?.throwIfAborted();
|
|
117
|
+
if (options.create !== undefined && (resolved === deviceDirectory || resolved === "/"))
|
|
118
|
+
throw new FsError("EISDIR", { syscall: "capabilitiesFor", path });
|
|
110
119
|
if (resolved === nullPath)
|
|
111
120
|
return deviceCapabilities;
|
|
112
121
|
if (resolved === deviceDirectory)
|
|
113
122
|
return { ...deviceCapabilities, readdir: true, write: false, append: false,
|
|
114
123
|
exclusiveCreate: false, streamingWrite: false, streamingAppend: false, descriptorWriteStream: false,
|
|
115
|
-
retainedRead: false, streamingRead: false, copy: false, exclusiveCopy: false };
|
|
116
|
-
const
|
|
124
|
+
retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
|
|
125
|
+
const query = this.#filesystem.capabilitiesFor;
|
|
126
|
+
options.signal?.throwIfAborted();
|
|
127
|
+
const selected = query === undefined || query === null ? undefined : await Reflect.apply(query, this.#filesystem, [path, options]);
|
|
128
|
+
options.signal?.throwIfAborted();
|
|
129
|
+
const observed = selected ?? this.#filesystem.capabilities;
|
|
130
|
+
options.signal?.throwIfAborted();
|
|
131
|
+
const capabilities = observed.retainedResize === true ? retainedResizeCapabilities(this.#filesystem, observed) : observed;
|
|
117
132
|
const unavailable = {};
|
|
118
133
|
if (typeof this.#filesystem.readStream !== "function")
|
|
119
134
|
unavailable.streamingRead = false;
|
|
@@ -124,24 +139,36 @@ export class DeviceFileSystem {
|
|
|
124
139
|
}
|
|
125
140
|
if (typeof this.#filesystem.openReadFile !== "function")
|
|
126
141
|
unavailable.retainedRead = false;
|
|
127
|
-
|
|
142
|
+
const result = Object.keys(unavailable).some(key => capabilities[key] !== false)
|
|
128
143
|
? { ...capabilities, ...unavailable } : capabilities;
|
|
144
|
+
options.signal?.throwIfAborted();
|
|
145
|
+
return result;
|
|
129
146
|
}
|
|
130
147
|
async stat(path, options = {}) {
|
|
131
148
|
const resolved = await this.#resolve(path, options);
|
|
149
|
+
options.signal?.throwIfAborted();
|
|
132
150
|
if (resolved === nullPath)
|
|
133
151
|
return { ...this.#nullStat };
|
|
134
152
|
if (resolved === deviceDirectory)
|
|
135
153
|
return { ...this.#directoryStat };
|
|
136
|
-
|
|
154
|
+
const stat = this.#filesystem.stat;
|
|
155
|
+
options.signal?.throwIfAborted();
|
|
156
|
+
const result = await Reflect.apply(stat, this.#filesystem, [path, options]);
|
|
157
|
+
options.signal?.throwIfAborted();
|
|
158
|
+
return result;
|
|
137
159
|
}
|
|
138
160
|
async lstat(path, options = {}) {
|
|
139
161
|
const resolved = await this.#resolve(path, options, false);
|
|
162
|
+
options.signal?.throwIfAborted();
|
|
140
163
|
if (resolved === nullPath)
|
|
141
164
|
return { ...this.#nullStat };
|
|
142
165
|
if (resolved === deviceDirectory)
|
|
143
166
|
return { ...this.#directoryStat };
|
|
144
|
-
|
|
167
|
+
const lstat = this.#filesystem.lstat;
|
|
168
|
+
options.signal?.throwIfAborted();
|
|
169
|
+
const result = await Reflect.apply(lstat, this.#filesystem, [path, options]);
|
|
170
|
+
options.signal?.throwIfAborted();
|
|
171
|
+
return result;
|
|
145
172
|
}
|
|
146
173
|
compareEntry(path, peer, peerPath, options = {}) {
|
|
147
174
|
return compareEntries(this, path, peer, peerPath, options);
|
|
@@ -183,9 +210,7 @@ export class DeviceFileSystem {
|
|
|
183
210
|
if (resolved !== nullPath) {
|
|
184
211
|
if (resolved === deviceDirectory)
|
|
185
212
|
throw new FsError("EISDIR", { path });
|
|
186
|
-
|
|
187
|
-
throw new FsError("ENOTSUP", { path });
|
|
188
|
-
return this.#filesystem.openReadFile(path, options);
|
|
213
|
+
return openRetainedReadFile(this.#filesystem, path, options);
|
|
189
214
|
}
|
|
190
215
|
const stat = this.#nullStat;
|
|
191
216
|
let closed = false;
|
|
@@ -194,9 +219,43 @@ export class DeviceFileSystem {
|
|
|
194
219
|
return {
|
|
195
220
|
async stat(settings = {}) { check(settings); return { ...stat }; },
|
|
196
221
|
async read(position, maxBytes, settings = {}) { check(settings); nonnegative(position, path); nonnegative(maxBytes, path); return new Uint8Array(); },
|
|
222
|
+
async seekEnd(settings = {}) { check(settings); return 0n; },
|
|
197
223
|
async close() { closed = true; },
|
|
198
224
|
};
|
|
199
225
|
}
|
|
226
|
+
async openResizeFile(path, options = {}) {
|
|
227
|
+
options.signal?.throwIfAborted();
|
|
228
|
+
const resolution = this.#resolve(path, options, true, options.create ?? false);
|
|
229
|
+
const signal = options.signal;
|
|
230
|
+
const resolved = await (signal ? new Promise((resolve, reject) => {
|
|
231
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
232
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
233
|
+
resolution.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
234
|
+
if (signal.aborted)
|
|
235
|
+
abort();
|
|
236
|
+
}) : resolution);
|
|
237
|
+
options.signal?.throwIfAborted();
|
|
238
|
+
if (resolved === deviceDirectory || resolved === "/")
|
|
239
|
+
throw new FsError("EISDIR", { syscall: "openResizeFile", path });
|
|
240
|
+
if (resolved !== nullPath)
|
|
241
|
+
return openRetainedResizeFile(this.#filesystem, path, options);
|
|
242
|
+
const stat = this.#nullStat;
|
|
243
|
+
let closing;
|
|
244
|
+
const check = (settings, syscall) => {
|
|
245
|
+
settings.signal?.throwIfAborted();
|
|
246
|
+
if (closing)
|
|
247
|
+
throw new FsError("EBADF", { syscall, path });
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
async stat(settings = {}) { check(settings, "fstat"); return { ...stat }; },
|
|
251
|
+
async seekEnd(settings = {}) { check(settings, "lseek"); return 0n; },
|
|
252
|
+
async truncate(_length, settings = {}) {
|
|
253
|
+
check(settings, "ftruncate");
|
|
254
|
+
throw new FsError("EINVAL", { syscall: "ftruncate", path });
|
|
255
|
+
},
|
|
256
|
+
close: () => closing ??= Promise.resolve(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
200
259
|
async writeFile(path, data, options = {}) {
|
|
201
260
|
if (await this.#writeTarget(path, options)) {
|
|
202
261
|
if (!(data instanceof Uint8Array))
|
|
@@ -2,4 +2,4 @@ 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): Promise<string>;
|
|
5
|
+
export declare function resolveDevicePath(filesystem: FileSystem, path: string, options: FsOptions, followFinal?: boolean, resizeCreate?: boolean): Promise<string>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
2
2
|
import { validatePath } from "../../contracts/virtual-path.js";
|
|
3
|
+
import { capturePathNamespace } from "../path-namespace.js";
|
|
3
4
|
export const nullPath = "/dev/null";
|
|
4
5
|
export const deviceDirectory = "/dev";
|
|
5
6
|
export function lexicalDevicePath(path) {
|
|
@@ -15,10 +16,104 @@ export function lexicalDevicePath(path) {
|
|
|
15
16
|
}
|
|
16
17
|
return `/${parts.join("/")}`;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
async function resolveResizeDevicePath(filesystem, path, options, create, namespace) {
|
|
20
|
+
validatePath(path);
|
|
21
|
+
const components = (value) => {
|
|
22
|
+
const result = value.split("/").filter(Boolean);
|
|
23
|
+
if (value.endsWith("/"))
|
|
24
|
+
result.push("");
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
const pending = components(path);
|
|
28
|
+
const parts = [];
|
|
29
|
+
let links = 0;
|
|
30
|
+
let expanded = path.length;
|
|
31
|
+
let boundary;
|
|
32
|
+
while (pending.length) {
|
|
33
|
+
options.signal?.throwIfAborted();
|
|
34
|
+
const component = pending.shift();
|
|
35
|
+
const current = `/${parts.join("/")}`;
|
|
36
|
+
if (current === nullPath)
|
|
37
|
+
throw new FsError("ENOTDIR", { path });
|
|
38
|
+
if (current !== deviceDirectory) {
|
|
39
|
+
const observe = filesystem.stat;
|
|
40
|
+
options.signal?.throwIfAborted();
|
|
41
|
+
const stat = await Reflect.apply(observe, filesystem, [current, options]);
|
|
42
|
+
options.signal?.throwIfAborted();
|
|
43
|
+
if (stat.type !== "directory")
|
|
44
|
+
throw new FsError("ENOTDIR", { path });
|
|
45
|
+
if (!(component === "" && pending.length === 0)) {
|
|
46
|
+
const access = filesystem.access;
|
|
47
|
+
options.signal?.throwIfAborted();
|
|
48
|
+
await Reflect.apply(access, filesystem, [current, 1, options]);
|
|
49
|
+
options.signal?.throwIfAborted();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (component === "." || component === "")
|
|
53
|
+
continue;
|
|
54
|
+
if (component === "..") {
|
|
55
|
+
if (boundary !== undefined && current === boundary)
|
|
56
|
+
throw new FsError("EACCES", { path });
|
|
57
|
+
parts.pop();
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (new TextEncoder().encode(component).byteLength > 255)
|
|
61
|
+
throw new FsError("ENAMETOOLONG", { path });
|
|
62
|
+
const candidate = `/${[...parts, component].join("/")}`;
|
|
63
|
+
const selected = namespace === undefined ? undefined : candidate === deviceDirectory || candidate === nullPath ? "/" : namespace(candidate);
|
|
64
|
+
if (boundary !== undefined && selected !== boundary)
|
|
65
|
+
throw new FsError("EACCES", { path });
|
|
66
|
+
if (create && pending.length === 1 && pending[0] === "")
|
|
67
|
+
throw new FsError("EISDIR", { path });
|
|
68
|
+
if (candidate !== deviceDirectory && candidate !== nullPath) {
|
|
69
|
+
let stat;
|
|
70
|
+
const lstat = filesystem.lstat;
|
|
71
|
+
options.signal?.throwIfAborted();
|
|
72
|
+
try {
|
|
73
|
+
stat = await Reflect.apply(lstat, filesystem, [candidate, options]);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
options.signal?.throwIfAborted();
|
|
77
|
+
if (!(isFsError(error, "ENOENT") && create && pending.length === 0))
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
options.signal?.throwIfAborted();
|
|
81
|
+
if (stat?.type === "symlink") {
|
|
82
|
+
boundary ??= selected;
|
|
83
|
+
const readlink = filesystem.readlink;
|
|
84
|
+
options.signal?.throwIfAborted();
|
|
85
|
+
if (typeof readlink !== "function")
|
|
86
|
+
throw new FsError("ENOTSUP", { path });
|
|
87
|
+
if (++links > 40)
|
|
88
|
+
throw new FsError("ELOOP", { path });
|
|
89
|
+
const target = await Reflect.apply(readlink, filesystem, [candidate, options]);
|
|
90
|
+
options.signal?.throwIfAborted();
|
|
91
|
+
validatePath(target);
|
|
92
|
+
expanded += target.length;
|
|
93
|
+
if (expanded > 65536)
|
|
94
|
+
throw new FsError("ENAMETOOLONG", { path });
|
|
95
|
+
if (target.startsWith("/"))
|
|
96
|
+
parts.splice(0, parts.length, ...(boundary ?? "/").split("/").filter(Boolean));
|
|
97
|
+
const targetParts = components(target);
|
|
98
|
+
if (targetParts.at(-1) === "" && pending[0] === "")
|
|
99
|
+
targetParts.pop();
|
|
100
|
+
pending.unshift(...targetParts);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
parts.push(component);
|
|
105
|
+
}
|
|
106
|
+
return `/${parts.join("/")}`;
|
|
107
|
+
}
|
|
108
|
+
export async function resolveDevicePath(filesystem, path, options, followFinal = true, resizeCreate) {
|
|
19
109
|
options.signal?.throwIfAborted();
|
|
110
|
+
const namespace = capturePathNamespace(filesystem, options);
|
|
111
|
+
if (resizeCreate !== undefined)
|
|
112
|
+
return resolveResizeDevicePath(filesystem, path, options, resizeCreate, namespace);
|
|
20
113
|
const lexical = lexicalDevicePath(path);
|
|
21
|
-
const
|
|
114
|
+
const lstat = filesystem.lstat;
|
|
115
|
+
options.signal?.throwIfAborted();
|
|
116
|
+
const aliases = typeof lstat === "function";
|
|
22
117
|
if (!aliases && lexical !== nullPath && lexical !== deviceDirectory && lexical !== "/")
|
|
23
118
|
return lexical;
|
|
24
119
|
const pending = path.split("/");
|
|
@@ -27,6 +122,7 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
27
122
|
let expanded = path.length;
|
|
28
123
|
let absolute = path.startsWith("/");
|
|
29
124
|
let traversalFailure;
|
|
125
|
+
let boundary;
|
|
30
126
|
while (pending.length) {
|
|
31
127
|
options.signal?.throwIfAborted();
|
|
32
128
|
if (`/${parts.join("/")}` === nullPath)
|
|
@@ -35,17 +131,22 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
35
131
|
if (!component || component === ".")
|
|
36
132
|
continue;
|
|
37
133
|
if (component === "..") {
|
|
134
|
+
if (boundary !== undefined && `/${parts.join("/")}` === boundary)
|
|
135
|
+
throw new FsError("EACCES", { path });
|
|
38
136
|
parts.pop();
|
|
39
137
|
continue;
|
|
40
138
|
}
|
|
41
139
|
const candidate = `/${[...parts, component].join("/")}`;
|
|
140
|
+
const selected = namespace === undefined ? undefined : candidate === deviceDirectory || candidate === nullPath ? "/" : namespace(candidate);
|
|
141
|
+
if (boundary !== undefined && selected !== boundary)
|
|
142
|
+
throw new FsError("EACCES", { path });
|
|
42
143
|
if (candidate !== deviceDirectory && candidate !== nullPath && ((followFinal && aliases) || pending.length)) {
|
|
43
144
|
const lookup = absolute ? candidate : candidate.slice(1);
|
|
44
145
|
let stat;
|
|
45
146
|
try {
|
|
46
|
-
if (typeof
|
|
147
|
+
if (typeof lstat !== "function")
|
|
47
148
|
throw new FsError("ENOTSUP", { path });
|
|
48
|
-
stat = await
|
|
149
|
+
stat = await Reflect.apply(lstat, filesystem, [lookup, options]);
|
|
49
150
|
}
|
|
50
151
|
catch (error) {
|
|
51
152
|
options.signal?.throwIfAborted();
|
|
@@ -58,18 +159,21 @@ export async function resolveDevicePath(filesystem, path, options, followFinal =
|
|
|
58
159
|
}
|
|
59
160
|
options.signal?.throwIfAborted();
|
|
60
161
|
if (stat?.type === "symlink") {
|
|
61
|
-
|
|
162
|
+
boundary ??= selected;
|
|
163
|
+
const readlink = filesystem.readlink;
|
|
164
|
+
options.signal?.throwIfAborted();
|
|
165
|
+
if (typeof readlink !== "function")
|
|
62
166
|
throw new FsError("ENOTSUP", { path });
|
|
63
167
|
if (++links > 40)
|
|
64
168
|
throw new FsError("ELOOP", { path });
|
|
65
|
-
const target = await
|
|
169
|
+
const target = await Reflect.apply(readlink, filesystem, [lookup, options]);
|
|
66
170
|
options.signal?.throwIfAborted();
|
|
67
171
|
validatePath(target);
|
|
68
172
|
expanded += target.length;
|
|
69
173
|
if (expanded > 65536)
|
|
70
174
|
throw new FsError("ENAMETOOLONG", { path });
|
|
71
175
|
if (target.startsWith("/")) {
|
|
72
|
-
parts.length
|
|
176
|
+
parts.splice(0, parts.length, ...(boundary ?? "/").split("/").filter(Boolean));
|
|
73
177
|
absolute = true;
|
|
74
178
|
}
|
|
75
179
|
pending.unshift(...target.split("/"));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
import { type MemoryFileSystemOptions } from "./limits.js";
|
|
4
4
|
export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
|
|
@@ -35,6 +35,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
35
35
|
atomicRename: true;
|
|
36
36
|
streamingRead: true;
|
|
37
37
|
retainedRead: true;
|
|
38
|
+
readonly retainedResize: boolean;
|
|
38
39
|
streamingWrite: true;
|
|
39
40
|
readonly descriptorWriteStream: boolean;
|
|
40
41
|
}>;
|
|
@@ -87,7 +88,9 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
87
88
|
chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
|
|
88
89
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|
|
89
90
|
truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
|
|
90
|
-
|
|
91
|
+
private resizeNode;
|
|
92
|
+
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
93
|
+
openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
|
|
91
94
|
readStream(path: string, options?: ReadStreamOptions): ByteSource;
|
|
92
95
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
93
96
|
}
|