@poe-platform/safe-fs 0.1.729 → 0.1.731

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.
@@ -18,10 +18,11 @@ export declare class MemoryLedger {
18
18
  export declare class MemoryAllocation {
19
19
  private references;
20
20
  readonly data: Uint8Array;
21
- private readonly ledger;
21
+ private ledger;
22
22
  constructor(data: Uint8Array, ledger: MemoryLedger);
23
23
  retain(): void;
24
24
  release(): void;
25
25
  isReleased64(): boolean;
26
- reuse(): void;
26
+ detachLedger(dummyLedger: MemoryLedger): void;
27
+ reuse(ledger?: MemoryLedger): void;
27
28
  }
@@ -68,13 +68,18 @@ export class MemoryLedger {
68
68
  }
69
69
  export class MemoryAllocation {
70
70
  constructor(data, ledger) {
71
+ this.references = 1;
71
72
  this.data = data;
72
73
  this.ledger = ledger;
73
74
  }
74
75
  retain() {
76
+ if (this.data.byteLength === 0)
77
+ return;
75
78
  this.references++;
76
79
  }
77
80
  release() {
81
+ if (this.data.byteLength === 0)
82
+ return;
78
83
  if (this.references <= 0)
79
84
  throw new Error("Memory allocation already released");
80
85
  if (--this.references === 0)
@@ -83,8 +88,13 @@ export class MemoryAllocation {
83
88
  isReleased64() {
84
89
  return this.references === 0 && this.data.byteLength === 64;
85
90
  }
86
- reuse() {
91
+ detachLedger(dummyLedger) {
92
+ this.ledger = dummyLedger;
93
+ }
94
+ reuse(ledger) {
87
95
  this.references = 1;
96
+ if (ledger !== undefined)
97
+ this.ledger = ledger;
88
98
  this.data.fill(0);
89
99
  }
90
100
  }
@@ -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)
@@ -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");
@@ -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 };
@@ -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.729",
3
+ "version": "0.1.731",
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
  }