@poe-platform/safe-fs 0.1.728 → 0.1.730

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.
@@ -3,20 +3,26 @@ export declare class MemoryLedger {
3
3
  readonly limits: Readonly<MemoryFileSystemLimits>;
4
4
  private retainedBytes;
5
5
  private metadataUnits;
6
+ readonly maxFileBytesSmi: number;
7
+ readonly maxRetainedBytesSmi: number;
8
+ readonly maxMetadataUnitsSmi: number;
9
+ readonly hasInfiniteRetained: boolean;
6
10
  constructor(limits: Readonly<MemoryFileSystemLimits>);
7
11
  get availableBytes(): number;
12
+ canPreallocate64(nameBytes: number): boolean;
8
13
  fileSize(length: number, syscall: string, path: string): void;
9
14
  check(bytes: number, units: number, syscall: string, path: string): void;
10
15
  reserve(bytes: number, units: number, syscall: string, path: string): void;
11
16
  release(bytes: number, units: number): void;
12
17
  }
13
18
  export declare class MemoryAllocation {
14
- readonly data: Uint8Array;
15
- private readonly ledger;
16
19
  private references;
20
+ readonly data: Uint8Array;
21
+ private ledger;
17
22
  constructor(data: Uint8Array, ledger: MemoryLedger);
18
23
  retain(): void;
19
24
  release(): void;
20
25
  isReleased64(): boolean;
21
- reuse(): void;
26
+ detachLedger(dummyLedger: MemoryLedger): void;
27
+ reuse(ledger?: MemoryLedger): void;
22
28
  }
@@ -1,21 +1,41 @@
1
1
  import { FsError } from "../../contracts/errors.js";
2
+ const MAX_SMI = 1073741823;
2
3
  export class MemoryLedger {
3
4
  limits;
4
5
  retainedBytes = 0;
5
6
  metadataUnits = 0;
7
+ maxFileBytesSmi;
8
+ maxRetainedBytesSmi;
9
+ maxMetadataUnitsSmi;
10
+ hasInfiniteRetained;
6
11
  constructor(limits) {
7
12
  this.limits = limits;
13
+ this.maxFileBytesSmi = limits.maxFileBytes >= MAX_SMI ? MAX_SMI : (limits.maxFileBytes | 0);
14
+ this.maxRetainedBytesSmi = limits.maxRetainedBytes >= MAX_SMI ? MAX_SMI : (limits.maxRetainedBytes | 0);
15
+ this.maxMetadataUnitsSmi = limits.maxMetadataUnits >= MAX_SMI ? MAX_SMI : (limits.maxMetadataUnits | 0);
16
+ this.hasInfiniteRetained = limits.maxRetainedBytes === Infinity;
8
17
  }
9
18
  get availableBytes() {
10
19
  const max = this.limits.maxRetainedBytes;
11
20
  return max === Infinity ? Infinity : max - this.retainedBytes;
12
21
  }
22
+ canPreallocate64(nameBytes) {
23
+ return this.maxFileBytesSmi >= 64 && (this.hasInfiniteRetained || this.maxRetainedBytesSmi - this.retainedBytes > 65536 + nameBytes);
24
+ }
13
25
  fileSize(length, syscall, path) {
26
+ if ((length | 0) === length && length >= 0 && length <= this.maxFileBytesSmi)
27
+ return;
14
28
  if (!Number.isSafeInteger(length) || length < 0 || length > this.limits.maxFileBytes) {
15
29
  throw new FsError("EFBIG", { syscall, path });
16
30
  }
17
31
  }
18
32
  check(bytes, units, syscall, path) {
33
+ if ((bytes | 0) === bytes && bytes >= 0 &&
34
+ (units | 0) === units && units >= 0 &&
35
+ this.retainedBytes <= this.maxRetainedBytesSmi - bytes &&
36
+ this.metadataUnits <= this.maxMetadataUnitsSmi - units) {
37
+ return;
38
+ }
19
39
  const maxRetained = this.limits.maxRetainedBytes;
20
40
  const maxUnits = this.limits.maxMetadataUnits;
21
41
  if (!Number.isSafeInteger(bytes) || bytes < 0 ||
@@ -26,6 +46,14 @@ export class MemoryLedger {
26
46
  }
27
47
  }
28
48
  reserve(bytes, units, syscall, path) {
49
+ if ((bytes | 0) === bytes && bytes >= 0 &&
50
+ (units | 0) === units && units >= 0 &&
51
+ this.retainedBytes <= this.maxRetainedBytesSmi - bytes &&
52
+ this.metadataUnits <= this.maxMetadataUnitsSmi - units) {
53
+ this.retainedBytes = (this.retainedBytes + bytes) | 0;
54
+ this.metadataUnits = (this.metadataUnits + units) | 0;
55
+ return;
56
+ }
29
57
  this.check(bytes, units, syscall, path);
30
58
  this.retainedBytes += bytes;
31
59
  this.metadataUnits += units;
@@ -39,9 +67,6 @@ export class MemoryLedger {
39
67
  }
40
68
  }
41
69
  export class MemoryAllocation {
42
- data;
43
- ledger;
44
- references = 1;
45
70
  constructor(data, ledger) {
46
71
  this.data = data;
47
72
  this.ledger = ledger;
@@ -58,8 +83,16 @@ export class MemoryAllocation {
58
83
  isReleased64() {
59
84
  return this.references === 0 && this.data.byteLength === 64;
60
85
  }
61
- reuse() {
86
+ detachLedger(dummyLedger) {
87
+ this.ledger = dummyLedger;
88
+ }
89
+ reuse(ledger) {
62
90
  this.references = 1;
91
+ if (ledger !== undefined)
92
+ this.ledger = ledger;
63
93
  this.data.fill(0);
64
94
  }
65
95
  }
96
+ Object.assign(MemoryAllocation.prototype, {
97
+ references: 1,
98
+ });
@@ -188,7 +188,7 @@ export class MountFileSystem {
188
188
  && await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
189
189
  : this.capabilities.conditionalChmod;
190
190
  const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
191
- const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
191
+ const withOpen = { ...ordinary, retainedStagingWrite: false, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
192
192
  const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
193
193
  : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
194
194
  if (location.synthetic)
@@ -36,7 +36,9 @@ function hasCode(error, code) {
36
36
  function fail(code, path, message) {
37
37
  throw new FsError(code, { syscall: "overlay", path, ...(message === undefined ? {} : { message }) });
38
38
  }
39
- function integer(value, path) {
39
+ function integer(value, path, unlimited = false) {
40
+ if (unlimited && value === Infinity)
41
+ return;
40
42
  if (!Number.isSafeInteger(value) || value < 0)
41
43
  fail("EINVAL", path);
42
44
  }
@@ -86,7 +88,7 @@ export class OverlayFileSystem {
86
88
  }, false));
87
89
  this.maxBufferBytes = options.maxBufferBytes ?? Infinity;
88
90
  if (options.maxBufferBytes !== undefined)
89
- integer(options.maxBufferBytes, "/");
91
+ integer(options.maxBufferBytes, "/", true);
90
92
  const writable = !this.#upper.capabilities.readOnly && this.#upper.capabilities.atomicRename === true;
91
93
  const readable = [this.#upper, this.#lower].map((backend) => retainedReadCapabilities(backend).retainedRead);
92
94
  const streamingRead = readable.every((capability) => capability === true) ? true
@@ -520,7 +522,7 @@ export class OverlayFileSystem {
520
522
  fail("EISDIR", entry.path);
521
523
  this.permission(entry, 4);
522
524
  if (options.maxBytes !== undefined)
523
- integer(options.maxBytes, entry.path);
525
+ integer(options.maxBytes, entry.path, true);
524
526
  const limit = Math.min(options.maxBytes ?? this.maxBufferBytes, this.maxBufferBytes);
525
527
  if (entry.stat.size > limit)
526
528
  fail("EFBIG", entry.path);
@@ -153,15 +153,15 @@ async function existingBytes(fs, path, options) {
153
153
  }
154
154
  }
155
155
  export function withFileSystemQuota(fs, options) {
156
- if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
157
- throw new RangeError("maxBytes must be a nonnegative safe integer");
156
+ if (options.maxBytes !== Infinity && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0))
157
+ throw new RangeError("maxBytes must be a nonnegative safe integer or Infinity");
158
158
  const scanLimits = {
159
159
  maxScanEntries: options.maxScanEntries ?? Infinity,
160
160
  maxScanDepth: options.maxScanDepth ?? Infinity,
161
161
  };
162
162
  for (const [name, value] of Object.entries({ maxScanEntries: options.maxScanEntries, maxScanDepth: options.maxScanDepth })) {
163
- if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
164
- throw new RangeError(`${name} must be a nonnegative safe integer`);
163
+ if (value !== undefined && value !== Infinity && (!Number.isSafeInteger(value) || value < 0))
164
+ throw new RangeError(`${name} must be a nonnegative safe integer or Infinity`);
165
165
  }
166
166
  let queue = Promise.resolve();
167
167
  let reservedBytes = 0;
@@ -1,3 +1,4 @@
1
+ import { platform } from "#safe-fs-platform";
1
2
  import { snapshotStagingCreation } from "../staging-cleanup.js";
2
3
  import { snapshotConditionalChmod } from "../conditional-chmod.js";
3
4
  import { runStagingGuard, snapshotDirectoryAncestry } from "../staging-ancestry.js";
@@ -7,6 +8,7 @@ import * as native from "node:fs/promises";
7
8
  import { dirname, basename, isAbsolute, join, relative, resolve, sep } from "node:path";
8
9
  import { nativeAllocatedBytes } from "./allocation.js";
9
10
  import { openFileDescriptor } from "../descriptor.js";
11
+ import { compareIdentity } from "../mount/identity.js";
10
12
  import { finishCleanup } from "../../contracts/cleanup.js";
11
13
  import { callNativeSeekEnd, loadNativeSeekBinding } from "../../node/native-seek.js";
12
14
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
@@ -32,11 +34,11 @@ function fileStat(stats) {
32
34
  mtimeMs: "mtimeNs" in stats ? Number(stats.mtimeNs / 1000000n) + Number(stats.mtimeNs % 1000000n) / 1e6 : stats.mtimeMs,
33
35
  ctimeMs: "ctimeNs" in stats ? Number(stats.ctimeNs / 1000000n) + Number(stats.ctimeNs % 1000000n) / 1e6 : stats.ctimeMs,
34
36
  birthtimeMs: "birthtimeNs" in stats ? Number(stats.birthtimeNs / 1000000n) + Number(stats.birthtimeNs % 1000000n) / 1e6 : stats.birthtimeMs, ino, dev,
35
- ...(Number.isSafeInteger(dev) && dev >= 0 && Number.isSafeInteger(ino) && ino >= 0
37
+ ...(Number.isSafeInteger(dev) && dev >= 0 && Number.isSafeInteger(ino) && ino > 0
36
38
  ? { identityScope: Symbol.for("virtual-bash.fs.native") } : {}),
37
39
  nlink: Number(stats.nlink), uid: Number(stats.uid), gid: Number(stats.gid),
38
40
  };
39
- if ("ctimeNs" in stats && !stats.isDirectory())
41
+ if (snapshot.identityScope !== undefined && "ctimeNs" in stats && (stats.ctimeNs !== 0n || stats.mtimeNs !== 0n) && !stats.isDirectory())
40
42
  Object.defineProperty(snapshot, "opaqueVersion", { value: `${stats.ctimeNs}:${stats.mtimeNs}`, enumerable: true });
41
43
  return snapshot;
42
44
  }
@@ -98,14 +100,13 @@ function nativeError(error) {
98
100
  */
99
101
  export class RealFileSystem {
100
102
  capabilities = Object.freeze({
101
- read: true, stat: true, readdir: true, realpath: true, access: true, open: true,
103
+ read: true, stat: true, readdir: true, realpath: true, access: true,
102
104
  write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
103
105
  mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
104
106
  rename: true, atomicRenameNoReplace: false, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
105
107
  streamingAppend: true, randomAccessWrite: true,
106
- readOnly: false, symlinks: true, hardlinks: true, permissions: true,
107
- conditionalChmod: true,
108
- timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true, trustedOwnedStaging: true,
108
+ readOnly: false, symlinks: true, hardlinks: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true,
109
+ ...platform.nativeFileSystem,
109
110
  });
110
111
  configuredRoot;
111
112
  renameNoReplace;
@@ -122,6 +123,8 @@ export class RealFileSystem {
122
123
  this.configuredRoot = root;
123
124
  this.renameNoReplace = typeof options === "string" ? undefined : options.renameNoReplace;
124
125
  if (this.renameNoReplace !== undefined) {
126
+ if (!platform.nativeFileSystem.atomicRename)
127
+ throw new FsError("ENOTSUP", { syscall: "root", message: "atomic rename is unavailable on this host" });
125
128
  if (typeof this.renameNoReplace !== "function")
126
129
  throw new FsError("EINVAL", { syscall: "root", message: "renameNoReplace must be a function" });
127
130
  this.capabilities = Object.freeze({ ...this.capabilities, atomicRenameNoReplace: true });
@@ -291,10 +294,15 @@ export class RealFileSystem {
291
294
  }
292
295
  }
293
296
  expectStaging(path, expected, identityOnly = false) {
297
+ if (!platform.nativeFileSystem.trustedOwnedStaging)
298
+ throw new FsError("ENOTSUP");
294
299
  if (expected && (expected.identityScope !== Symbol.for("virtual-bash.fs.native")
295
300
  || expected.dev === undefined || expected.ino === undefined || !identityOnly && expected.opaqueVersion === undefined))
296
301
  throw new FsError("ENOTSUP");
297
302
  const current = this.stagingSnapshot(path);
303
+ if (expected && current && (current.identityScope !== Symbol.for("virtual-bash.fs.native")
304
+ || current.dev === undefined || current.ino === undefined || !identityOnly && current.opaqueVersion === undefined))
305
+ throw new FsError("ENOTSUP");
298
306
  if (expected === null ? current !== null : !current || current.dev !== expected.dev || current.ino !== expected.ino || current.type !== expected.type
299
307
  || !identityOnly && (current.opaqueVersion !== expected.opaqueVersion || current.size !== expected.size || current.mode !== expected.mode || current.nlink !== expected.nlink))
300
308
  throw new FsError("EAGAIN");
@@ -521,6 +529,8 @@ export class RealFileSystem {
521
529
  }
522
530
  }
523
531
  open(path, options) {
532
+ if (!platform.nativeFileSystem.open)
533
+ return this.operation("open", path, options, async () => { throw new FsError("ENOTSUP"); });
524
534
  return openFileDescriptor(path, options, {
525
535
  noFollow: true, positionedRead: true, positionedWrite: true, truncate: true, synchronization: "storage",
526
536
  }, async (admitted) => {
@@ -663,7 +673,7 @@ export class RealFileSystem {
663
673
  return this.operation("mkdir", path, options, async () => {
664
674
  if (options.mode !== undefined)
665
675
  integer(options.mode);
666
- if (options.exactMode && options.recursive)
676
+ if (options.exactMode && (!platform.nativeFileSystem.permissions || options.recursive))
667
677
  throw new FsError("ENOTSUP");
668
678
  const target = await this.path(path, {
669
679
  ...options,
@@ -746,8 +756,13 @@ export class RealFileSystem {
746
756
  }
747
757
  if (target && options.exclusive)
748
758
  throw new FsError("EEXIST");
749
- if (target && origin.isFile() && origin.dev === target.dev && origin.ino === target.ino)
750
- throw new FsError("EINVAL");
759
+ if (target && origin.isFile()) {
760
+ const identity = compareIdentity(fileStat(origin), fileStat(target));
761
+ if (identity === "same")
762
+ throw new FsError("EINVAL");
763
+ if (identity === "unknown")
764
+ throw new FsError("ENOTSUP");
765
+ }
751
766
  if (!target)
752
767
  flags |= constants.COPYFILE_EXCL;
753
768
  }
@@ -813,6 +828,8 @@ export class RealFileSystem {
813
828
  }
814
829
  async chmod(path, mode, options = {}) {
815
830
  return this.operation("chmod", path, options, async () => {
831
+ if (!platform.nativeFileSystem.permissions)
832
+ throw new FsError("ENOTSUP");
816
833
  integer(mode);
817
834
  const conditional = snapshotConditionalChmod(path, options);
818
835
  if (conditional) {
@@ -846,6 +863,8 @@ export class RealFileSystem {
846
863
  }
847
864
  async utimes(path, atimeMs, mtimeMs, options = {}) {
848
865
  return this.operation("utimes", path, options, async () => {
866
+ if (!platform.nativeFileSystem.timestamps)
867
+ throw new FsError("ENOTSUP");
849
868
  if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs)
850
869
  || Math.abs(atimeMs) > 8.64e15 || Math.abs(mtimeMs) > 8.64e15)
851
870
  throw new FsError("EINVAL");
@@ -59,7 +59,7 @@ function fail(code, syscall, path, message) {
59
59
  throw new FsError(code, { syscall, path, ...(message === undefined ? {} : { message }) });
60
60
  }
61
61
  function validateLimit(value, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
62
- if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
62
+ if ((value !== Infinity || maximum !== Infinity) && (!Number.isSafeInteger(value) || value < minimum || value > maximum)) {
63
63
  throw new FsError("EINVAL", { message: `${name} must be an integer from ${minimum} to ${maximum}` });
64
64
  }
65
65
  return value;
@@ -130,14 +130,14 @@ export class S3FileSystem {
130
130
  this.bucket = options.bucket;
131
131
  this.allowRename = options.allowNonAtomicRename ?? true;
132
132
  this.pageSize = validateLimit(options.pageSize ?? 1000, "pageSize", 1, 1000);
133
- this.maxReadBytes = options.maxReadBytes === undefined ? Infinity : validateLimit(options.maxReadBytes, "maxReadBytes", 0);
134
- this.maxStreamBytes = options.maxStreamBytes === undefined ? Infinity : validateLimit(options.maxStreamBytes, "maxStreamBytes", 0);
135
- this.maxListEntries = options.maxListEntries === undefined ? Infinity : validateLimit(options.maxListEntries, "maxListEntries", 1);
136
- this.maxRequests = options.maxRequests === undefined ? Infinity : validateLimit(options.maxRequests, "maxRequests", 1);
133
+ this.maxReadBytes = options.maxReadBytes === undefined ? Infinity : validateLimit(options.maxReadBytes, "maxReadBytes", 0, Infinity);
134
+ this.maxStreamBytes = options.maxStreamBytes === undefined ? Infinity : validateLimit(options.maxStreamBytes, "maxStreamBytes", 0, Infinity);
135
+ this.maxListEntries = options.maxListEntries === undefined ? Infinity : validateLimit(options.maxListEntries, "maxListEntries", 1, Infinity);
136
+ this.maxRequests = options.maxRequests === undefined ? Infinity : validateLimit(options.maxRequests, "maxRequests", 1, Infinity);
137
137
  this.removalLimits = {
138
- maxRequests: validateLimit(options.removalLimits?.maxRequests ?? 32, "removalLimits.maxRequests", 1),
139
- maxListEntries: validateLimit(options.removalLimits?.maxListEntries ?? 32, "removalLimits.maxListEntries", 1),
140
- maxDeleteObjects: validateLimit(options.removalLimits?.maxDeleteObjects ?? 16, "removalLimits.maxDeleteObjects", 1),
138
+ maxRequests: validateLimit(options.removalLimits?.maxRequests ?? Infinity, "removalLimits.maxRequests", 1, Infinity),
139
+ maxListEntries: validateLimit(options.removalLimits?.maxListEntries ?? Infinity, "removalLimits.maxListEntries", 1, Infinity),
140
+ maxDeleteObjects: validateLimit(options.removalLimits?.maxDeleteObjects ?? Infinity, "removalLimits.maxDeleteObjects", 1, Infinity),
141
141
  };
142
142
  this.capabilities = Object.freeze({
143
143
  open: false,
@@ -427,7 +427,7 @@ export class S3FileSystem {
427
427
  return this.stat(path, options);
428
428
  }
429
429
  async body(output, path, options) {
430
- const limit = Math.min(this.maxReadBytes, options.maxBytes === undefined ? this.maxReadBytes : validateLimit(options.maxBytes, "maxBytes", 0));
430
+ const limit = Math.min(this.maxReadBytes, options.maxBytes === undefined ? this.maxReadBytes : validateLimit(options.maxBytes, "maxBytes", 0, Infinity));
431
431
  let iterator;
432
432
  return this.call("readFile", path, options, async () => {
433
433
  if (output.Body && Symbol.asyncIterator in output.Body)
@@ -473,7 +473,7 @@ export class S3FileSystem {
473
473
  async readFile(input, options = {}) {
474
474
  const path = this.path(input);
475
475
  if (options.maxBytes !== undefined)
476
- validateLimit(options.maxBytes, "maxBytes", 0);
476
+ validateLimit(options.maxBytes, "maxBytes", 0, Infinity);
477
477
  const info = await this.stat(input, options);
478
478
  if (info.type === "directory")
479
479
  fail("EISDIR", "readFile", path);
@@ -1,4 +1,5 @@
1
1
  import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
2
+ export declare function retargetScopedFileSystem(scoped: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void, maxPathComponents?: number): boolean;
2
3
  export declare function scopeFileSystem(filesystem: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void, options?: {
3
4
  readonly preserveDescriptorWriteReceipt?: boolean;
4
5
  readonly creationMask?: number;
@@ -8,6 +8,14 @@ import { openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabi
8
8
  import { createStagingCleanup, snapshotStagingCreation } from "./staging-cleanup.js";
9
9
  import { inspectStagingBindings, runStagingGuard, snapshotDirectoryAncestry, snapshotStagingResolution } from "./staging-ancestry.js";
10
10
  const originals = new WeakMap();
11
+ const retargeters = new WeakMap();
12
+ export function retargetScopedFileSystem(scoped, charge, signal, cleanupCharge = charge, maxPathComponents) {
13
+ const fn = retargeters.get(scoped);
14
+ if (!fn)
15
+ return false;
16
+ fn(charge, signal, cleanupCharge, maxPathComponents);
17
+ return true;
18
+ }
11
19
  const operations = new Set([
12
20
  "prepareDirectoryAncestry", "prepareStagingResolution",
13
21
  "publishFileConditional", "removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
@@ -19,7 +27,7 @@ const operations = new Set([
19
27
  export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = charge, options = {}) {
20
28
  const original = originals.get(filesystem)?.filesystem ?? filesystem;
21
29
  const creationMask = options.creationMask ?? originals.get(filesystem)?.creationMask;
22
- const maxPathComponents = options.maxPathComponents ?? originals.get(filesystem)?.maxPathComponents;
30
+ let maxPathComponents = options.maxPathComponents ?? originals.get(filesystem)?.maxPathComponents;
23
31
  if (creationMask !== undefined && (!Number.isInteger(creationMask) || creationMask < 0 || creationMask > 0o777))
24
32
  throw new RangeError("creationMask must be a permission mask between 0 and 0777");
25
33
  const creationOptions = (settings, directory = false) => {
@@ -343,7 +351,20 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
343
351
  controls.signal?.throwIfAborted();
344
352
  return backendCleanup.remove(controls);
345
353
  }, () => backendCleanup.close());
346
- return Object.freeze({ ...staging, cleanup });
354
+ const backendWriter = staging.writer;
355
+ const writer = backendWriter && Object.freeze({
356
+ write: async (bytes, settings = {}) => {
357
+ const controls = resizeOptions(settings);
358
+ admit(controls);
359
+ await backendWriter.write(bytes, controls);
360
+ },
361
+ finish: async (settings = {}) => {
362
+ const controls = resizeOptions(settings);
363
+ admit(controls);
364
+ return backendWriter.finish(controls);
365
+ },
366
+ });
367
+ return Object.freeze({ ...staging, cleanup, ...(writer ? { writer } : {}) });
347
368
  }
348
369
  catch (error) {
349
370
  await finishCleanup(() => backendCleanup.close(), true);
@@ -471,7 +492,17 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
471
492
  return scoped;
472
493
  },
473
494
  });
474
- originals.set(view, { filesystem: original, signal, cleanupCharge, creationMask, maxPathComponents });
495
+ const originalRecord = { filesystem: original, signal, cleanupCharge, creationMask, maxPathComponents };
496
+ originals.set(view, originalRecord);
497
+ retargeters.set(view, (nextCharge, nextSignal, nextCleanupCharge, nextMaxPathComponents) => {
498
+ charge = nextCharge;
499
+ signal = nextSignal;
500
+ cleanupCharge = nextCleanupCharge;
501
+ maxPathComponents = nextMaxPathComponents ?? originals.get(filesystem)?.maxPathComponents;
502
+ originalRecord.signal = nextSignal;
503
+ originalRecord.cleanupCharge = nextCleanupCharge;
504
+ originalRecord.maxPathComponents = maxPathComponents;
505
+ });
475
506
  registerEntryView(view, async (path, options) => {
476
507
  assertOpen(options);
477
508
  return { filesystem: original, path };
@@ -86,7 +86,9 @@ const resourceIdBody = '<?xml version="1.0" encoding="utf-8"?>'
86
86
  function fail(code, syscall, path, message) {
87
87
  throw new FsError(code, { syscall, path, ...(message === undefined ? {} : { message }) });
88
88
  }
89
- function positive(value, name, zero = false) {
89
+ function positive(value, name, zero = false, unlimited = false) {
90
+ if (unlimited && value === Infinity)
91
+ return value;
90
92
  if (!Number.isSafeInteger(value) || value < (zero ? 0 : 1)) {
91
93
  throw new FsError("EINVAL", { message: `${name} must be a ${zero ? "nonnegative" : "positive"} safe integer` });
92
94
  }
@@ -286,9 +288,9 @@ export class WebDavFileSystem {
286
288
  removeDirectory: this.atomicEmptyDirectory !== undefined,
287
289
  streamingAppend: this.requestStreamSupport !== false,
288
290
  });
289
- this.maxResponseBytes = options.maxResponseBytes === undefined ? 16 * 1024 * 1024 : positive(options.maxResponseBytes, "maxResponseBytes");
290
- this.maxXmlBytes = options.maxXmlBytes === undefined ? 1024 * 1024 : positive(options.maxXmlBytes, "maxXmlBytes");
291
- this.maxEntries = options.maxEntries === undefined ? Infinity : positive(options.maxEntries, "maxEntries");
291
+ this.maxResponseBytes = options.maxResponseBytes === undefined ? 16 * 1024 * 1024 : positive(options.maxResponseBytes, "maxResponseBytes", false, true);
292
+ this.maxXmlBytes = options.maxXmlBytes === undefined ? 1024 * 1024 : positive(options.maxXmlBytes, "maxXmlBytes", false, true);
293
+ this.maxEntries = options.maxEntries === undefined ? Infinity : positive(options.maxEntries, "maxEntries", false, true);
292
294
  this.timeoutMs = options.timeoutMs === undefined ? undefined : positive(options.timeoutMs, "timeoutMs");
293
295
  this.overwritePolicy = options.overwritePolicy ?? "lock";
294
296
  this.configuredComparison = options.compareEntry !== undefined;
@@ -867,7 +869,7 @@ export class WebDavFileSystem {
867
869
  }
868
870
  async readFile(path, options = {}) {
869
871
  const normalized = normalize(path);
870
- const limit = Math.min(this.maxResponseBytes, options.maxBytes === undefined ? this.maxResponseBytes : positive(options.maxBytes, "maxBytes", true));
872
+ const limit = Math.min(this.maxResponseBytes, options.maxBytes === undefined ? this.maxResponseBytes : positive(options.maxBytes, "maxBytes", true, true));
871
873
  if ((await this.stat(path, options)).type === "directory")
872
874
  fail("EISDIR", "readFile", path);
873
875
  return this.request("GET", normalized, options, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
@@ -0,0 +1 @@
1
+ export declare function loadBinding(): Promise<never>;
@@ -0,0 +1,5 @@
1
+ import { FsError } from "../contracts/errors.js";
2
+ // Workers support native file reads, but cannot load the optional Node addon.
3
+ export async function loadBinding() {
4
+ throw new FsError("ENOTSUP", { message: "Native seek is unavailable in Workers" });
5
+ }
@@ -3,6 +3,14 @@ import type { ScopedTransportBudgetFrame } from "./transport-budget.js";
3
3
  export type PlatformErrno = number | undefined;
4
4
  export type PlatformComparisonCallback<Callback> = Callback & never;
5
5
  export declare const platform: Readonly<{
6
+ nativeFileSystem: Readonly<{
7
+ open: false;
8
+ permissions: false;
9
+ timestamps: false;
10
+ conditionalChmod: false;
11
+ trustedOwnedStaging: false;
12
+ atomicRename: false;
13
+ }>;
6
14
  maxCollectionBytes: number;
7
15
  errno(_code: string): PlatformErrno;
8
16
  callbackAuthorities: false;
@@ -1,5 +1,6 @@
1
1
  import { FsError } from "../contracts/errors.js";
2
2
  export const platform = Object.freeze({
3
+ nativeFileSystem: Object.freeze({ open: false, permissions: false, timestamps: false, conditionalChmod: false, trustedOwnedStaging: false, atomicRename: false }),
3
4
  maxCollectionBytes: 32 * 1024 * 1024,
4
5
  errno(_code) {
5
6
  return undefined;
@@ -4,6 +4,14 @@ import { type ScopedTransportBudgetFrame } from "./transport-budget.js";
4
4
  export type PlatformErrno = number;
5
5
  export type PlatformComparisonCallback<Callback> = Callback;
6
6
  export declare const platform: Readonly<{
7
+ nativeFileSystem: Readonly<{
8
+ open: true;
9
+ permissions: true;
10
+ timestamps: true;
11
+ conditionalChmod: true;
12
+ trustedOwnedStaging: true;
13
+ atomicRename: true;
14
+ }>;
7
15
  maxCollectionBytes: number;
8
16
  errno(code: string): PlatformErrno;
9
17
  callbackAuthorities: true;
@@ -16,6 +16,7 @@ const systemErrnos = (() => {
16
16
  })();
17
17
  const negotiating = new AsyncLocalStorage();
18
18
  export const platform = Object.freeze({
19
+ nativeFileSystem: Object.freeze({ open: true, permissions: true, timestamps: true, conditionalChmod: true, trustedOwnedStaging: true, atomicRename: true }),
19
20
  maxCollectionBytes: Infinity,
20
21
  errno(code) {
21
22
  const errno = systemErrnos.get(code === "EOPNOTSUPP" ? "ENOTSUP" : code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.728",
3
+ "version": "0.1.730",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -69,9 +69,11 @@
69
69
  },
70
70
  "./fs/real": {
71
71
  "types": {
72
+ "workerd": "./dist/safe-fs/fs/real/index.d.ts",
72
73
  "browser": "./dist/safe-fs/node-unavailable.d.ts",
73
74
  "default": "./dist/safe-fs/fs/real/index.d.ts"
74
75
  },
76
+ "workerd": "./dist/safe-fs/fs/real/index.js",
75
77
  "browser": null,
76
78
  "import": "./dist/safe-fs/fs/real/index.js"
77
79
  },
@@ -134,15 +136,17 @@
134
136
  "imports": {
135
137
  "#safe-fs-platform": {
136
138
  "types": {
139
+ "workerd": "./dist/safe-fs/platform/browser.d.ts",
137
140
  "browser": "./dist/safe-fs/platform/browser.d.ts",
138
141
  "default": "./dist/safe-fs/platform/node.d.ts"
139
142
  },
143
+ "workerd": "./dist/safe-fs/platform/browser.js",
140
144
  "browser": "./dist/safe-fs/platform/browser.js",
141
145
  "default": "./dist/safe-fs/platform/node.js"
142
146
  },
143
147
  "#safe-fs-native-seek": {
144
148
  "types": "./dist/safe-fs/native/fs-seek/loader.d.ts",
145
- "workerd": null,
149
+ "workerd": "./dist/safe-fs/node/native-seek-unavailable.js",
146
150
  "browser": null,
147
151
  "default": "./dist/safe-fs/native/fs-seek/loader.mjs"
148
152
  }