@poe-platform/safe-fs 0.1.511 → 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
@@ -1,5 +1,6 @@
1
1
  import { FsError } from "../../contracts/errors.js";
2
- import { openRetainedReadFile, quotaCapabilities, retainedReadCapabilities } from "../capabilities.js";
2
+ import { finishCleanup } from "../../contracts/cleanup.js";
3
+ import { openRetainedReadFile, openRetainedResizeFile, quotaCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
3
4
  import { admitDirectoryEntries } from "../directory-admission.js";
4
5
  export class FileSystemQuotaError extends Error {
5
6
  maxBytes;
@@ -9,8 +10,25 @@ export class FileSystemQuotaError extends Error {
9
10
  this.name = "FileSystemQuotaError";
10
11
  }
11
12
  }
12
- async function usedBytes(fs, limits, options, change) {
13
+ function completeIdentity(stat) {
14
+ return (typeof stat.identityScope === "symbol" || typeof stat.identityScope === "object" && stat.identityScope !== null)
15
+ && [stat.dev, stat.ino].every(value => typeof value === "number" && Number.isSafeInteger(value) && value >= 0);
16
+ }
17
+ function namespaceMetadata(metadata, signal) {
18
+ if (!signal)
19
+ return metadata;
20
+ return new Promise((resolve, reject) => {
21
+ const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
22
+ signal.addEventListener("abort", abort, { once: true });
23
+ metadata.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
24
+ if (signal.aborted)
25
+ abort();
26
+ });
27
+ }
28
+ async function usedBytes(fs, limits, options, change, retained = change?.retained === true) {
13
29
  let total = 0;
30
+ let retainedTotal = 0n;
31
+ let shrinkCredited = false;
14
32
  let possibleAliases = 0;
15
33
  let remaining = limits.maxScanEntries;
16
34
  const pending = [{ path: "/", depth: 0 }];
@@ -20,7 +38,8 @@ async function usedBytes(fs, limits, options, change) {
20
38
  if (!directory)
21
39
  break;
22
40
  const signal = options?.signal;
23
- const entries = await fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), maxEntries: remaining });
41
+ const listing = fs.readdir(directory.path, { ...options, ...(signal ? { signal } : {}), maxEntries: remaining });
42
+ const entries = await (retained ? namespaceMetadata(listing, signal) : listing);
24
43
  options?.signal?.throwIfAborted();
25
44
  const count = entries.length;
26
45
  admitDirectoryEntries(count, remaining, directory.path);
@@ -41,7 +60,32 @@ async function usedBytes(fs, limits, options, change) {
41
60
  pending.push({ path, depth });
42
61
  }
43
62
  else {
44
- const stat = await fs.lstat(path, options);
63
+ const metadata = fs.lstat(path, options);
64
+ const stat = await (retained ? namespaceMetadata(metadata, signal) : metadata);
65
+ if (retained) {
66
+ options?.signal?.throwIfAborted();
67
+ if (!Number.isSafeInteger(stat.size) || stat.size < 0)
68
+ throw new FsError("EIO", { syscall: "lstat", path, message: "invalid quota entry size" });
69
+ retainedTotal += BigInt(stat.size);
70
+ if (!change || stat.type !== "file")
71
+ continue;
72
+ const known = completeIdentity(stat);
73
+ const same = known && change.stat.identityScope === stat.identityScope && change.stat.dev === stat.dev && change.stat.ino === stat.ino;
74
+ const nextBytes = change.stat.size + change.delta;
75
+ if (same) {
76
+ possibleAliases++;
77
+ retainedTotal += BigInt(Math.max(0, nextBytes - stat.size));
78
+ if (!shrinkCredited && change.delta < 0) {
79
+ retainedTotal -= BigInt(Math.max(0, Math.min(stat.size, change.stat.size) - nextBytes));
80
+ shrinkCredited = true;
81
+ }
82
+ }
83
+ else if (!known) {
84
+ possibleAliases++;
85
+ retainedTotal += BigInt(Math.max(0, change.delta, nextBytes - stat.size));
86
+ }
87
+ continue;
88
+ }
45
89
  total += stat.size;
46
90
  if (!change || stat.type !== "file")
47
91
  continue;
@@ -66,6 +110,11 @@ async function usedBytes(fs, limits, options, change) {
66
110
  }
67
111
  }
68
112
  }
113
+ if (retained) {
114
+ if (change && possibleAliases === 0)
115
+ retainedTotal += BigInt(Math.max(0, change.delta));
116
+ return Number(retainedTotal);
117
+ }
69
118
  if (change && possibleAliases === 0)
70
119
  total += Math.max(0, change.delta);
71
120
  return total;
@@ -98,6 +147,24 @@ export function withFileSystemQuota(fs, options) {
98
147
  queue = result.catch(() => undefined);
99
148
  return result;
100
149
  };
150
+ const resizeBackend = new Proxy(Object.create(fs), {
151
+ get(_target, property) {
152
+ const original = Reflect.get(fs, property);
153
+ if (property === "openResizeFile" && typeof original === "function")
154
+ return async (path, resizeOptions) => {
155
+ resizeOptions.signal?.throwIfAborted();
156
+ if (resizeOptions.create) {
157
+ const total = await usedBytes(fs, scanLimits, resizeOptions, undefined, true);
158
+ resizeOptions.signal?.throwIfAborted();
159
+ if (total > options.maxBytes)
160
+ throw new FileSystemQuotaError(options.maxBytes);
161
+ }
162
+ resizeOptions.signal?.throwIfAborted();
163
+ return Reflect.apply(original, fs, [path, resizeOptions]);
164
+ };
165
+ return typeof original === "function" ? original.bind(fs) : original;
166
+ },
167
+ });
101
168
  const assertDelta = async (path, nextBytes, fsOptions) => {
102
169
  fsOptions?.signal?.throwIfAborted();
103
170
  let current;
@@ -116,6 +183,75 @@ export function withFileSystemQuota(fs, options) {
116
183
  throw new FileSystemQuotaError(options.maxBytes);
117
184
  };
118
185
  const mutations = {
186
+ openResizeFile(path, resizeOptions = {}) {
187
+ return mutate(async () => {
188
+ let retained;
189
+ try {
190
+ retained = await openRetainedResizeFile(resizeBackend, path, resizeOptions);
191
+ const pinned = { ...await retained.stat(resizeOptions) };
192
+ resizeOptions.signal?.throwIfAborted();
193
+ if (pinned.type !== "file" || !completeIdentity(pinned))
194
+ throw new FsError("ENOTSUP", { syscall: "openResizeFile", path, message: "quota requires complete retained file identity" });
195
+ if (!Number.isSafeInteger(pinned.size) || pinned.size < 0)
196
+ throw new FsError("EIO", { syscall: "stat", path, message: "invalid retained file size" });
197
+ const handle = retained;
198
+ const seekEnd = handle.seekEnd;
199
+ resizeOptions.signal?.throwIfAborted();
200
+ let pending = Promise.resolve();
201
+ let closing;
202
+ const admit = (operation, operationOptions) => {
203
+ if (closing)
204
+ return Promise.reject(new FsError("EBADF", { path }));
205
+ const result = mutate(async () => {
206
+ operationOptions?.signal?.throwIfAborted();
207
+ const value = await operation();
208
+ operationOptions?.signal?.throwIfAborted();
209
+ return value;
210
+ });
211
+ pending = result.catch(() => undefined);
212
+ return result;
213
+ };
214
+ const current = async (operationOptions) => {
215
+ const stat = { ...await handle.stat(operationOptions) };
216
+ operationOptions?.signal?.throwIfAborted();
217
+ if (stat.type !== "file" || !completeIdentity(stat) || stat.identityScope !== pinned.identityScope || stat.dev !== pinned.dev || stat.ino !== pinned.ino
218
+ || !Number.isSafeInteger(stat.size) || stat.size < 0)
219
+ throw new FsError("EIO", { syscall: "stat", path, message: "invalid retained quota identity or size" });
220
+ return stat;
221
+ };
222
+ return {
223
+ stat(operationOptions) { return admit(() => current(operationOptions), operationOptions); },
224
+ ...(typeof seekEnd === "function" ? {
225
+ seekEnd(operationOptions) {
226
+ return admit(() => Reflect.apply(seekEnd, handle, [operationOptions]), operationOptions);
227
+ },
228
+ } : {}),
229
+ truncate(length, operationOptions) {
230
+ return admit(async () => {
231
+ if (!Number.isSafeInteger(length) || length < 0)
232
+ throw new FsError("EINVAL", { syscall: "truncate", path });
233
+ const stat = await current(operationOptions);
234
+ const projected = await usedBytes(fs, scanLimits, operationOptions, { path, stat, delta: length - stat.size, retained: true });
235
+ operationOptions?.signal?.throwIfAborted();
236
+ if (projected > options.maxBytes)
237
+ throw new FileSystemQuotaError(options.maxBytes);
238
+ await handle.truncate(length, operationOptions);
239
+ }, operationOptions);
240
+ },
241
+ close() {
242
+ closing ??= pending.then(() => handle.close());
243
+ return closing;
244
+ },
245
+ };
246
+ }
247
+ catch (error) {
248
+ if (retained)
249
+ await finishCleanup(() => retained.close(), true);
250
+ resizeOptions.signal?.throwIfAborted();
251
+ throw error;
252
+ }
253
+ });
254
+ },
119
255
  writeFile(path, data, writeOptions) {
120
256
  return mutate(async () => {
121
257
  const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
@@ -183,11 +319,11 @@ export function withFileSystemQuota(fs, options) {
183
319
  if (property === "canonicalizeMissingTarget")
184
320
  return undefined;
185
321
  if (property === "capabilities")
186
- return quotaCapabilities(retainedReadCapabilities(fs));
322
+ return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs)));
187
323
  if (property === "capabilitiesFor")
188
324
  return async (path, fsOptions) => {
189
325
  const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
190
- return quotaCapabilities(retainedReadCapabilities(fs, capabilities));
326
+ return quotaCapabilities(retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)));
191
327
  };
192
328
  if (property === "openReadFile")
193
329
  return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
@@ -1,11 +1,11 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileResizeHandle, FileStat, OpenReadFileOptions, OpenResizeFileOptions, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
3
  export declare class ReadOnlyFileSystem implements FileSystem {
4
4
  #private;
5
5
  constructor(filesystem: FileSystem);
6
6
  get capabilities(): FileSystemCapabilities;
7
- capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
8
- openReadFile(path: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
7
+ capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
8
+ openReadFile(path: string, options?: OpenReadFileOptions): Promise<import("../../contracts/filesystem.js").FileReadHandle>;
9
9
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
10
10
  stat(path: string, options?: FsOptions): Promise<FileStat>;
11
11
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
@@ -29,5 +29,6 @@ export declare class ReadOnlyFileSystem implements FileSystem {
29
29
  chmod(path: string, _mode: number, _options?: FsOptions): Promise<void>;
30
30
  utimes(path: string, _atimeMs: number, _mtimeMs: number, _options?: FsOptions): Promise<void>;
31
31
  truncate(path: string, _length?: number, _options?: FsOptions): Promise<void>;
32
+ openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
32
33
  }
33
34
  export declare function createReadOnlyFileSystem(filesystem: FileSystem): ReadOnlyFileSystem;
@@ -4,14 +4,16 @@ import { readBytes } from "../../contracts/io.js";
4
4
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
5
5
  import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities } from "../capabilities.js";
6
6
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
7
+ import { pathNamespace, readOnlyPathNamespace } from "../path-namespace.js";
7
8
  function readOnly(syscall, path, dest) {
8
9
  throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
9
10
  }
10
11
  function snapshotStat(stat) {
11
- const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
12
+ const { type, size, allocatedBytes, preferredIoBlockSize, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
12
13
  return {
13
14
  type, size, mode, mtimeMs, atimeMs, ctimeMs,
14
15
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
16
+ ...(preferredIoBlockSize === undefined ? {} : { preferredIoBlockSize }),
15
17
  ...(birthtimeMs === undefined ? {} : { birthtimeMs }),
16
18
  ...(identityScope === undefined ? {} : { identityScope }),
17
19
  ...(ino === undefined ? {} : { ino }),
@@ -26,6 +28,10 @@ export class ReadOnlyFileSystem {
26
28
  #capabilities;
27
29
  constructor(filesystem) {
28
30
  this.#filesystem = filesystem;
31
+ if (Reflect.has(filesystem, pathNamespace))
32
+ Object.defineProperty(this, pathNamespace, {
33
+ get: () => readOnlyPathNamespace(Reflect.get(filesystem, pathNamespace)),
34
+ });
29
35
  registerEntryView(this, async (path) => ({ filesystem: this.#filesystem, path, readOnly: true }));
30
36
  const streamingRead = typeof filesystem.readStream === "function" ? filesystem.capabilities.streamingRead : false;
31
37
  this.#capabilities = readOnlyCapabilities({
@@ -45,6 +51,9 @@ export class ReadOnlyFileSystem {
45
51
  return this.#capabilities;
46
52
  }
47
53
  async capabilitiesFor(path, options) {
54
+ options?.signal?.throwIfAborted();
55
+ if (options?.create !== undefined)
56
+ readOnly("capabilitiesFor", path);
48
57
  const capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
49
58
  return readOnlyCapabilities(retainedReadCapabilities(this.#filesystem, capabilities));
50
59
  }
@@ -138,6 +147,10 @@ export class ReadOnlyFileSystem {
138
147
  async truncate(path, _length, _options) {
139
148
  readOnly("truncate", path);
140
149
  }
150
+ async openResizeFile(path, options = {}) {
151
+ options.signal?.throwIfAborted();
152
+ readOnly("openResizeFile", path);
153
+ }
141
154
  }
142
155
  export function createReadOnlyFileSystem(filesystem) {
143
156
  return new ReadOnlyFileSystem(filesystem);
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
1
+ import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, OpenReadFileOptions, OpenResizeFileOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
2
2
  export interface RealFileSystemOptions {
3
3
  /** An existing, absolute host directory. It is never created implicitly. */
4
4
  readonly root: string;
@@ -69,7 +69,8 @@ export declare class RealFileSystem implements FileSystem {
69
69
  chmod(path: string, mode: number, options?: FsOptions): Promise<void>;
70
70
  utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
71
71
  truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
72
- openReadFile(path: string, options?: FsOptions): Promise<FileReadHandle>;
72
+ openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
73
+ openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
73
74
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
74
75
  writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
75
76
  }
@@ -3,6 +3,7 @@ import * as native from "node:fs/promises";
3
3
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { nativeAllocatedBytes } from "./allocation.js";
5
5
  import { finishCleanup } from "../../contracts/cleanup.js";
6
+ import { callNativeSeekEnd, loadNativeSeekBinding } from "../../node/native-seek.js";
6
7
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
7
8
  import { FsError, collectBytes, isErrnoCode, toByteSource, toFsError, validatePath, } from "../../contracts/index.js";
8
9
  function fileType(stats) {
@@ -16,9 +17,11 @@ function fileType(stats) {
16
17
  }
17
18
  function fileStat(stats) {
18
19
  const allocatedBytes = nativeAllocatedBytes(stats.blocks, process.platform);
20
+ const preferredIoBlockSize = stats.blksize;
19
21
  return {
20
22
  type: fileType(stats), size: stats.size, mode: stats.mode,
21
23
  ...(allocatedBytes === undefined ? {} : { allocatedBytes }),
24
+ ...(Number.isSafeInteger(preferredIoBlockSize) && preferredIoBlockSize > 0 ? { preferredIoBlockSize } : {}),
22
25
  atimeMs: stats.atimeMs, mtimeMs: stats.mtimeMs, ctimeMs: stats.ctimeMs,
23
26
  birthtimeMs: stats.birthtimeMs, ino: stats.ino, dev: stats.dev,
24
27
  ...(Number.isSafeInteger(stats.dev) && stats.dev >= 0 && Number.isSafeInteger(stats.ino) && stats.ino >= 0
@@ -87,7 +90,7 @@ export class RealFileSystem {
87
90
  rename: true, atomicRenameNoReplace: false, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
88
91
  streamingAppend: true, randomAccessWrite: true,
89
92
  readOnly: false, symlinks: true, hardlinks: true, permissions: true,
90
- timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true,
93
+ timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true,
91
94
  });
92
95
  configuredRoot;
93
96
  rootPromise;
@@ -136,7 +139,13 @@ export class RealFileSystem {
136
139
  while (pending.length > 0) {
137
140
  options.signal?.throwIfAborted();
138
141
  const { name: component, fromLink } = pending.shift();
139
- if (component === "" || component === ".")
142
+ if (component === "")
143
+ continue;
144
+ if ((component === "." || component === "..") && !options.checkTarget) {
145
+ await native.lstat(`${current}/.`);
146
+ options.signal?.throwIfAborted();
147
+ }
148
+ if (component === ".")
140
149
  continue;
141
150
  if (component === "..") {
142
151
  if (current === root) {
@@ -148,6 +157,11 @@ export class RealFileSystem {
148
157
  }
149
158
  continue;
150
159
  }
160
+ if (options.createFile && pending.length > 0 && pending.every((part) => part.name === "")) {
161
+ await native.lstat(`${current}/.`);
162
+ options.signal?.throwIfAborted();
163
+ throw new FsError("EISDIR");
164
+ }
151
165
  const candidate = join(current, component);
152
166
  let stats;
153
167
  try {
@@ -476,20 +490,23 @@ export class RealFileSystem {
476
490
  }
477
491
  });
478
492
  }
479
- async openReadFile(path, options = {}) {
493
+ async openResizeFile(path, options = {}) {
480
494
  options.signal?.throwIfAborted();
481
495
  const assertStock = () => {
482
- const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
496
+ const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openResizeFile", path }); };
483
497
  if (Object.getPrototypeOf(this) !== RealFileSystem.prototype)
484
498
  unsupported();
485
- for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
499
+ for (const name of ["openResizeFile", "truncate", "writeFile", "writeStream", "appendFile", "stat", "lstat", "realpath", "access",
486
500
  "root", "absoluteTarget", "walk", "path"]) {
487
501
  const descriptor = Object.getOwnPropertyDescriptor(this, name)
488
502
  ?? Object.getOwnPropertyDescriptor(RealFileSystem.prototype, name);
489
503
  if (!descriptor || !("value" in descriptor) || descriptor.value !== realImplementation[name]?.value)
490
504
  unsupported();
491
505
  }
492
- if (Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead !== true)
506
+ const capabilities = Object.getOwnPropertyDescriptor(this, "capabilities")?.value;
507
+ if (capabilities?.readOnly === true)
508
+ throw new FsError("EROFS", { syscall: "openResizeFile", path });
509
+ if (capabilities?.retainedResize !== true)
493
510
  unsupported();
494
511
  };
495
512
  assertStock();
@@ -497,10 +514,19 @@ export class RealFileSystem {
497
514
  ? new FsError(nativeError(error).code, { syscall, path }) : error;
498
515
  let resource;
499
516
  try {
500
- const target = await this.path(path, options);
517
+ if (options.mode !== undefined)
518
+ integer(options.mode);
519
+ if (options.create !== undefined && typeof options.create !== "boolean")
520
+ throw new FsError("EINVAL");
521
+ const target = await this.path(path, {
522
+ ...options, ...(options.create === true ? { missing: "final" } : {}),
523
+ deferTrailingSeparator: true, createFile: options.create === true,
524
+ });
501
525
  options.signal?.throwIfAborted();
502
526
  assertStock();
503
- resource = await native.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
527
+ const flags = constants.O_WRONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
528
+ | (options.create === true ? constants.O_CREAT : 0);
529
+ resource = await native.open(target, flags, options.mode ?? 0o666);
504
530
  options.signal?.throwIfAborted();
505
531
  const stats = await resource.stat();
506
532
  options.signal?.throwIfAborted();
@@ -513,6 +539,127 @@ export class RealFileSystem {
513
539
  catch (error) {
514
540
  await finishCleanup(async () => { await resource?.close(); }, true);
515
541
  options.signal?.throwIfAborted();
542
+ throw failure(error, "openResizeFile");
543
+ }
544
+ const handle = resource;
545
+ const pending = new Set();
546
+ let accepting = true;
547
+ let closing;
548
+ const assertOpen = (options, syscall) => {
549
+ options.signal?.throwIfAborted();
550
+ if (!accepting)
551
+ throw new FsError("EBADF", { syscall, path });
552
+ };
553
+ const perform = async (options, syscall, action) => {
554
+ assertOpen(options, syscall);
555
+ let settled;
556
+ const admitted = new Promise(resolve => { settled = resolve; });
557
+ pending.add(admitted);
558
+ try {
559
+ const value = await action();
560
+ options.signal?.throwIfAborted();
561
+ return value;
562
+ }
563
+ catch (error) {
564
+ options.signal?.throwIfAborted();
565
+ throw failure(error, syscall);
566
+ }
567
+ finally {
568
+ pending.delete(admitted);
569
+ settled();
570
+ }
571
+ };
572
+ return {
573
+ stat(options = {}) {
574
+ return perform(options, "fstat", async () => fileStat(await handle.stat()));
575
+ },
576
+ async truncate(length, options = {}) {
577
+ assertOpen(options, "ftruncate");
578
+ try {
579
+ integer(length);
580
+ }
581
+ catch (error) {
582
+ throw failure(error, "ftruncate");
583
+ }
584
+ return perform(options, "ftruncate", async () => { await handle.truncate(length); });
585
+ },
586
+ async seekEnd(options = {}) {
587
+ const signal = options.signal;
588
+ const operationOptions = signal === undefined ? {} : { signal };
589
+ assertOpen(operationOptions, "lseek");
590
+ try {
591
+ const binding = await loadNativeSeekBinding(signal);
592
+ assertOpen(operationOptions, "lseek");
593
+ const seek = binding.seekEnd;
594
+ assertOpen(operationOptions, "lseek");
595
+ if (typeof seek !== "function")
596
+ throw new FsError("EIO");
597
+ const descriptor = handle.fd;
598
+ assertOpen(operationOptions, "lseek");
599
+ return await perform(operationOptions, "lseek", () => callNativeSeekEnd(binding, seek, descriptor));
600
+ }
601
+ catch (error) {
602
+ signal?.throwIfAborted();
603
+ throw failure(error, "lseek");
604
+ }
605
+ },
606
+ close() {
607
+ accepting = false;
608
+ closing ??= Promise.all([...pending]).then(async () => {
609
+ try {
610
+ await handle.close();
611
+ }
612
+ catch (error) {
613
+ throw failure(error, "close");
614
+ }
615
+ });
616
+ return closing;
617
+ },
618
+ };
619
+ }
620
+ async openReadFile(path, options = {}) {
621
+ const signal = options.signal;
622
+ signal?.throwIfAborted();
623
+ const assertStock = () => {
624
+ const unsupported = () => { throw new FsError("ENOTSUP", { syscall: "openReadFile", path }); };
625
+ if (Object.getPrototypeOf(this) !== RealFileSystem.prototype)
626
+ unsupported();
627
+ for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
628
+ "root", "absoluteTarget", "walk", "path"]) {
629
+ const descriptor = Object.getOwnPropertyDescriptor(this, name)
630
+ ?? Object.getOwnPropertyDescriptor(RealFileSystem.prototype, name);
631
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== realImplementation[name]?.value)
632
+ unsupported();
633
+ }
634
+ if (Object.getOwnPropertyDescriptor(this, "capabilities")?.value?.retainedRead !== true)
635
+ unsupported();
636
+ };
637
+ assertStock();
638
+ const failure = (error, syscall) => error
639
+ ? new FsError(nativeError(error).code, { syscall, path }) : error;
640
+ let resource;
641
+ let directory = false;
642
+ try {
643
+ const allowDirectory = options.allowDirectory === true;
644
+ signal?.throwIfAborted();
645
+ const target = await this.path(path, signal === undefined ? {} : { signal });
646
+ signal?.throwIfAborted();
647
+ assertStock();
648
+ resource = await native.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
649
+ signal?.throwIfAborted();
650
+ const stats = await resource.stat();
651
+ signal?.throwIfAborted();
652
+ assertStock();
653
+ directory = stats.isDirectory();
654
+ if (directory && !allowDirectory)
655
+ throw new FsError("EISDIR");
656
+ if (!directory && !stats.isFile())
657
+ throw new FsError("ENOTSUP");
658
+ signal?.throwIfAborted();
659
+ }
660
+ catch (error) {
661
+ await finishCleanup(async () => { await resource?.close(); }, true);
662
+ signal?.throwIfAborted();
516
663
  throw failure(error, "openReadFile");
517
664
  }
518
665
  const handle = resource;
@@ -558,6 +705,8 @@ export class RealFileSystem {
558
705
  }
559
706
  if (maxBytes > Number.MAX_SAFE_INTEGER - position)
560
707
  throw new FsError("EINVAL", { syscall: "read", path });
708
+ if (directory)
709
+ throw new FsError("EISDIR", { syscall: "read", path });
561
710
  return perform(options, "read", async () => {
562
711
  let bytes;
563
712
  try {
@@ -572,6 +721,26 @@ export class RealFileSystem {
572
721
  return bytes.slice(0, bytesRead);
573
722
  });
574
723
  },
724
+ async seekEnd(options = {}) {
725
+ const signal = options.signal;
726
+ const operationOptions = signal === undefined ? {} : { signal };
727
+ assertOpen(operationOptions, "lseek");
728
+ try {
729
+ const binding = await loadNativeSeekBinding(signal);
730
+ assertOpen(operationOptions, "lseek");
731
+ const seek = binding.seekEnd;
732
+ assertOpen(operationOptions, "lseek");
733
+ if (typeof seek !== "function")
734
+ throw new FsError("EIO");
735
+ const descriptor = handle.fd;
736
+ assertOpen(operationOptions, "lseek");
737
+ return await perform(operationOptions, "lseek", () => callNativeSeekEnd(binding, seek, descriptor));
738
+ }
739
+ catch (error) {
740
+ signal?.throwIfAborted();
741
+ throw failure(error, "lseek");
742
+ }
743
+ },
575
744
  close() {
576
745
  accepting = false;
577
746
  closing ??= Promise.all([...pending]).then(async () => {