@poe-platform/safe-fs 0.1.512 → 0.1.513

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.
Files changed (31) hide show
  1. package/dist/safe-fs/bridge/stats.js +1 -1
  2. package/dist/safe-fs/contracts/errors.d.ts +1 -0
  3. package/dist/safe-fs/contracts/errors.js +1 -0
  4. package/dist/safe-fs/contracts/filesystem.d.ts +22 -2
  5. package/dist/safe-fs/fs/capabilities.d.ts +4 -2
  6. package/dist/safe-fs/fs/capabilities.js +80 -9
  7. package/dist/safe-fs/fs/devices/index.d.ts +4 -3
  8. package/dist/safe-fs/fs/devices/index.js +75 -16
  9. package/dist/safe-fs/fs/devices/path.d.ts +1 -1
  10. package/dist/safe-fs/fs/devices/path.js +111 -7
  11. package/dist/safe-fs/fs/memory/index.d.ts +5 -2
  12. package/dist/safe-fs/fs/memory/index.js +107 -10
  13. package/dist/safe-fs/fs/mount/index.d.ts +4 -3
  14. package/dist/safe-fs/fs/mount/index.js +66 -14
  15. package/dist/safe-fs/fs/overlay/index.d.ts +3 -3
  16. package/dist/safe-fs/fs/overlay/index.js +9 -3
  17. package/dist/safe-fs/fs/path-namespace.d.ts +4 -0
  18. package/dist/safe-fs/fs/path-namespace.js +63 -0
  19. package/dist/safe-fs/fs/quota/index.js +142 -6
  20. package/dist/safe-fs/fs/readonly/index.d.ts +4 -3
  21. package/dist/safe-fs/fs/readonly/index.js +14 -1
  22. package/dist/safe-fs/fs/real/index.d.ts +3 -2
  23. package/dist/safe-fs/fs/real/index.js +177 -8
  24. package/dist/safe-fs/fs/scoped.js +111 -13
  25. package/dist/safe-fs/native/fs-seek/linux-x64-glibc.node +0 -0
  26. package/dist/safe-fs/native/fs-seek/loader.d.ts +5 -0
  27. package/dist/safe-fs/native/fs-seek/loader.mjs +120 -0
  28. package/dist/safe-fs/native/fs-seek/manifest.json +46 -0
  29. package/dist/safe-fs/node/native-seek.d.ts +3 -0
  30. package/dist/safe-fs/node/native-seek.js +54 -0
  31. package/package.json +7 -1
@@ -19,7 +19,7 @@ export function bridgeStats(stat) {
19
19
  gid: stat.gid ?? 0,
20
20
  rdev: 0,
21
21
  size: stat.size,
22
- blksize: 4096,
22
+ blksize: stat.preferredIoBlockSize ?? 4096,
23
23
  blocks: Math.ceil(stat.size / 512),
24
24
  atimeMs: stat.atimeMs,
25
25
  mtimeMs: stat.mtimeMs,
@@ -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
  };
@@ -26,6 +26,7 @@ const descriptions = {
26
26
  EPERM: "operation not permitted",
27
27
  EPIPE: "broken pipe",
28
28
  EROFS: "read-only file system",
29
+ ESPIPE: "invalid seek",
29
30
  ETIMEDOUT: "operation timed out",
30
31
  EXDEV: "cross-device link not permitted",
31
32
  };
@@ -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?: FsOptions): Promise<FileReadHandle>;
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?: FsOptions): Promise<FileSystemCapabilities>;
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, FsOptions } from "../contracts/filesystem.js";
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: FsOptions): Promise<FileReadHandle>;
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
- options.signal?.throwIfAborted();
10
- if (typeof filesystem.openReadFile !== "function")
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 capabilities = await filesystem.capabilitiesFor?.(path, options) ?? filesystem.capabilities;
13
- options.signal?.throwIfAborted();
14
- if (retainedReadCapabilities(filesystem, capabilities).retainedRead !== true)
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
- handle = await filesystem.openReadFile(path, options);
17
- options.signal?.throwIfAborted();
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
- options.signal?.throwIfAborted();
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?: FsOptions): Promise<FileSystemCapabilities>;
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?: FsOptions): Promise<FileReadHandle>;
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
- this.#nullStat = Object.freeze({ type: "character", mode: 0o020666, size: 0, allocatedBytes: 0,
55
- mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1 });
56
- this.#directoryStat = Object.freeze({ ...this.#nullStat, type: "directory", mode: 0o040755, ino: 2 });
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 capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
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
- return Object.keys(unavailable).some(key => capabilities[key] !== false)
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
- return this.#filesystem.stat(path, options);
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
- return this.#filesystem.lstat(path, options);
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
- if (!this.#filesystem.openReadFile)
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
- export async function resolveDevicePath(filesystem, path, options, followFinal = true) {
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 aliases = typeof filesystem.lstat === "function";
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 filesystem.lstat !== "function")
147
+ if (typeof lstat !== "function")
47
148
  throw new FsError("ENOTSUP", { path });
48
- stat = await filesystem.lstat(lookup, options);
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
- if (typeof filesystem.readlink !== "function")
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 filesystem.readlink(lookup, options);
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 = 0;
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
- openReadFile(path: string, options?: FsOptions): Promise<FileReadHandle>;
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
  }