@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,10 +1,11 @@
1
1
  import { FsError } from "../contracts/errors.js";
2
2
  import { finishCleanup } from "../contracts/cleanup.js";
3
3
  import { registerEntryView } from "./mount/comparison.js";
4
+ import { openRetainedResizeFile, retainedResizeCapabilities } from "./capabilities.js";
4
5
  const originals = new WeakMap();
5
6
  const operations = new Set([
6
7
  "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
7
- "copyFile", "link", "lstat", "mkdir", "openReadFile", "readFile", "readStream", "readdir",
8
+ "copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
8
9
  "readlink", "realpath", "rename", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
9
10
  "writeFile", "writeStream",
10
11
  ]);
@@ -19,11 +20,98 @@ export function scopeFileSystem(filesystem, charge, signal) {
19
20
  assertOpen(options);
20
21
  charge();
21
22
  };
22
- const wrapHandle = (handle) => ({
23
- async stat(options) { admit(options); return handle.stat(options); },
24
- async read(position, maxBytes, options) { admit(options); return handle.read(position, maxBytes, options); },
25
- close: handle.close.bind(handle),
26
- });
23
+ const wrapHandle = (handle) => {
24
+ let closed = false;
25
+ const close = handle.close.bind(handle);
26
+ return {
27
+ async stat(options) { admit(options); return handle.stat(options); },
28
+ async read(position, maxBytes, options) { admit(options); return handle.read(position, maxBytes, options); },
29
+ get seekEnd() {
30
+ const seek = handle.seekEnd;
31
+ if (typeof seek !== "function")
32
+ return undefined;
33
+ return async (options = {}) => {
34
+ assertOpen(options);
35
+ if (closed)
36
+ throw new FsError("EBADF");
37
+ admit(options);
38
+ assertOpen(options);
39
+ if (closed)
40
+ throw new FsError("EBADF");
41
+ try {
42
+ const result = await Reflect.apply(seek, handle, [resizeOptions(options)]);
43
+ assertOpen(options);
44
+ return result;
45
+ }
46
+ catch (error) {
47
+ assertOpen(options);
48
+ throw error;
49
+ }
50
+ };
51
+ },
52
+ close() { closed = true; return close(); },
53
+ };
54
+ };
55
+ const resizeOptions = (options) => {
56
+ const scopedOptions = {
57
+ ...options, signal: options.signal ? AbortSignal.any([signal, options.signal]) : signal,
58
+ };
59
+ scopedOptions.signal.throwIfAborted();
60
+ return scopedOptions;
61
+ };
62
+ const wrapResizeHandle = (handle) => {
63
+ let closing;
64
+ const admitOperation = (options) => {
65
+ assertOpen(options);
66
+ if (closing)
67
+ throw new FsError("EBADF");
68
+ admit(options);
69
+ assertOpen(options);
70
+ if (closing)
71
+ throw new FsError("EBADF");
72
+ return resizeOptions(options);
73
+ };
74
+ return {
75
+ async stat(options = {}) {
76
+ try {
77
+ const result = await handle.stat(admitOperation(options));
78
+ assertOpen(options);
79
+ return result;
80
+ }
81
+ catch (error) {
82
+ assertOpen(options);
83
+ throw error;
84
+ }
85
+ },
86
+ async truncate(length, options = {}) {
87
+ try {
88
+ await handle.truncate(length, admitOperation(options));
89
+ assertOpen(options);
90
+ }
91
+ catch (error) {
92
+ assertOpen(options);
93
+ throw error;
94
+ }
95
+ },
96
+ get seekEnd() {
97
+ const seek = handle.seekEnd;
98
+ if (typeof seek !== "function")
99
+ return undefined;
100
+ return async (options = {}) => {
101
+ try {
102
+ const result = await Reflect.apply(seek, handle, [admitOperation(options)]);
103
+ assertOpen(options);
104
+ return result;
105
+ }
106
+ catch (error) {
107
+ assertOpen(options);
108
+ throw error;
109
+ }
110
+ };
111
+ },
112
+ close: () => closing ??= Promise.resolve().then(() => handle.close()),
113
+ };
114
+ };
27
115
  const wrapStream = (source, options) => ({
28
116
  [Symbol.asyncIterator]() {
29
117
  assertOpen(options);
@@ -62,6 +150,8 @@ export function scopeFileSystem(filesystem, charge, signal) {
62
150
  return Reflect.set(original, property, value, original);
63
151
  },
64
152
  get(_target, property) {
153
+ if (property === "capabilities")
154
+ return retainedResizeCapabilities(original);
65
155
  const method = Reflect.get(original, property, original);
66
156
  if (typeof method !== "function")
67
157
  return method;
@@ -90,13 +180,21 @@ export function scopeFileSystem(filesystem, charge, signal) {
90
180
  })();
91
181
  return Reflect.apply(method, original, args);
92
182
  };
93
- const scoped = property === "openReadFile"
94
- ? async (...args) => wrapHandle(await dispatch(...args))
95
- : property === "readStream"
96
- ? (...args) => wrapStream(dispatch(...args), args[1])
97
- : operations.has(property) && property !== "canonicalizeMissingTarget"
98
- ? async (...args) => dispatch(...args)
99
- : dispatch;
183
+ const scoped = property === "openResizeFile"
184
+ ? async (path, options = {}) => {
185
+ admit(options);
186
+ assertOpen(options);
187
+ return wrapResizeHandle(await openRetainedResizeFile(original, path, resizeOptions(options)));
188
+ }
189
+ : property === "capabilitiesFor"
190
+ ? async (...args) => retainedResizeCapabilities(original, await dispatch(...args))
191
+ : property === "openReadFile"
192
+ ? async (...args) => wrapHandle(await dispatch(...args))
193
+ : property === "readStream"
194
+ ? (...args) => wrapStream(dispatch(...args), args[1])
195
+ : operations.has(property) && property !== "canonicalizeMissingTarget"
196
+ ? async (...args) => dispatch(...args)
197
+ : dispatch;
100
198
  methods.set(property, { original: method, scoped });
101
199
  return scoped;
102
200
  },
@@ -0,0 +1,5 @@
1
+ export interface NativeSeekBinding {
2
+ seekEnd(descriptor: number): Promise<{ offset: bigint; errno: number }>;
3
+ }
4
+
5
+ export function loadBinding(): Promise<NativeSeekBinding>;
@@ -0,0 +1,120 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { open } from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+ import process from "node:process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const maxManifestBytes = 16384;
10
+ const maxBinaryBytes = 1048576;
11
+ let loading;
12
+
13
+ function failure(code, message) {
14
+ return Object.assign(new Error(message), { code });
15
+ }
16
+
17
+ function identifier(value) {
18
+ return typeof value === "string" && value.length > 0 && value.length <= 32
19
+ && [...value].every(character => "abcdefghijklmnopqrstuvwxyz0123456789_".includes(character));
20
+ }
21
+
22
+ function decimal(value) {
23
+ return typeof value === "string" && value.length > 0 && value.length <= 6
24
+ && [...value].every(character => "0123456789".includes(character));
25
+ }
26
+
27
+ function libcVersion(value) {
28
+ if (typeof value !== "string") return undefined;
29
+ const parts = value.split(".");
30
+ return parts.length === 2 && parts.every(decimal) ? parts.map(Number) : undefined;
31
+ }
32
+
33
+ function atLeast(version, minimum) {
34
+ return version[0] > minimum[0] || version[0] === minimum[0] && version[1] >= minimum[1];
35
+ }
36
+
37
+ async function readAsset(filename, limit) {
38
+ const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
39
+ let failed = false;
40
+ let primary;
41
+ let bytes;
42
+ try {
43
+ const stat = await handle.stat();
44
+ if (!stat.isFile()) throw failure("EINVAL", "Native asset must be a regular file");
45
+ if (!Number.isSafeInteger(stat.size) || stat.size <= 0) throw failure("EINVAL", "Invalid native asset size");
46
+ if (stat.size > limit) throw failure("EFBIG", "Native asset exceeds its byte limit");
47
+ bytes = Buffer.alloc(stat.size);
48
+ let offset = 0;
49
+ while (offset < bytes.length) {
50
+ const remaining = bytes.length - offset;
51
+ const { bytesRead } = await handle.read(bytes, offset, remaining, offset);
52
+ if (!Number.isSafeInteger(bytesRead) || bytesRead <= 0 || bytesRead > remaining) throw failure("EIO", "Incomplete native asset read");
53
+ offset += bytesRead;
54
+ }
55
+ const { bytesRead } = await handle.read(Buffer.alloc(1), 0, 1, offset);
56
+ if (bytesRead !== 0) throw failure("EIO", "Native asset changed while reading");
57
+ } catch (error) {
58
+ failed = true;
59
+ primary = error;
60
+ }
61
+ try { await handle.close(); }
62
+ catch (error) { if (!failed) throw error; }
63
+ if (failed) throw primary;
64
+ return bytes;
65
+ }
66
+
67
+ function validateManifest(value) {
68
+ const invalid = () => { throw failure("EINVAL", "Invalid native seek manifest"); };
69
+ if (value === null || typeof value !== "object" || Array.isArray(value)
70
+ || value.version !== 1 || value.napi !== 6 || value.maxBinaryBytes !== maxBinaryBytes
71
+ || !Array.isArray(value.targets)) invalid();
72
+ const selected = new Set();
73
+ for (const target of value.targets) {
74
+ if (target === null || typeof target !== "object" || Array.isArray(target)
75
+ || !identifier(target.platform) || !identifier(target.arch) || !identifier(target.libc)
76
+ || !Number.isSafeInteger(target.size) || target.size <= 0 || target.size > maxBinaryBytes
77
+ || typeof target.sha256 !== "string" || target.sha256.length !== 64
78
+ || ![...target.sha256].every(character => "0123456789abcdef".includes(character))) invalid();
79
+ const minimum = libcVersion(target.minimumLibc);
80
+ if (!minimum || !atLeast(minimum, [2, 31])) invalid();
81
+ const key = `${target.platform}-${target.arch}-${target.libc}`;
82
+ if (selected.has(key)) invalid();
83
+ selected.add(key);
84
+ }
85
+ return value;
86
+ }
87
+
88
+ async function load() {
89
+ const platform = process.platform;
90
+ const arch = process.arch;
91
+ const napi = process.versions.napi;
92
+ if (platform !== "linux" || arch !== "x64" || !decimal(napi) || Number(napi) < 6) {
93
+ throw failure("ENOTSUP", "Native seek is unavailable for this runtime");
94
+ }
95
+ const report = process.report;
96
+ const getReport = report?.getReport;
97
+ if (typeof getReport !== "function") throw failure("ENOTSUP", "Native seek requires a known glibc runtime");
98
+ const version = libcVersion(Reflect.apply(getReport, report, [])?.header?.glibcVersionRuntime);
99
+ if (!version || !atLeast(version, [2, 31])) throw failure("ENOTSUP", "Native seek requires glibc 2.31 or newer");
100
+ const manifestPath = fileURLToPath(new URL("manifest.json", import.meta.url));
101
+ const manifest = validateManifest(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await readAsset(manifestPath, maxManifestBytes))));
102
+ const target = manifest.targets.find(candidate => candidate.platform === platform && candidate.arch === arch && candidate.libc === "glibc");
103
+ if (!target || !atLeast(version, libcVersion(target.minimumLibc))) throw failure("ENOTSUP", "Native seek has no compatible asset");
104
+ const filename = fileURLToPath(new URL(`${target.platform}-${target.arch}-${target.libc}.node`, import.meta.url));
105
+ const bytes = await readAsset(filename, maxBinaryBytes);
106
+ if (bytes.length !== target.size || createHash("sha256").update(bytes).digest("hex") !== target.sha256) {
107
+ throw failure("EIO", "Native seek asset integrity check failed");
108
+ }
109
+ const require = createRequire(import.meta.url);
110
+ const binding = require(filename);
111
+ if (binding === null || typeof binding !== "object" || typeof binding.seekEnd !== "function") {
112
+ throw failure("EIO", "Invalid native seek binding");
113
+ }
114
+ return binding;
115
+ }
116
+
117
+ export function loadBinding() {
118
+ loading ??= Promise.resolve().then(load);
119
+ return loading;
120
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "version": 1,
3
+ "napi": 6,
4
+ "maxBinaryBytes": 1048576,
5
+ "targets": [
6
+ {
7
+ "platform": "linux",
8
+ "arch": "x64",
9
+ "libc": "glibc",
10
+ "minimumLibc": "2.31",
11
+ "size": 17064,
12
+ "sha256": "7e0b6e24f5e49a0a058e3d95c2aee8b438a753d4f36052537b91084a81ef0d53"
13
+ }
14
+ ],
15
+ "build": {
16
+ "sourceSha256": "e689d83731bc7ce2ab9632d5927f7f6c8af05a5258b057efa9c1566a888ced6e",
17
+ "loaderSha256": "337c49bc6744b137633eeb1fd19c19568fdc6d5eb0dfdc34d6bf45dd02573e38",
18
+ "declarationSha256": "19105e59e4bcc7a1706f74d261a5b09d2404eccaa27cc0bbc9d2af0dc9ef3684",
19
+ "headers": {
20
+ "version": "1.9.0",
21
+ "files": {
22
+ "node_api.h": {
23
+ "size": 10370,
24
+ "sha256": "2d4560831e525b47b060ec8a0864ab73993df9e20215ce9f0fe7b24cd31af32a"
25
+ },
26
+ "node_api_types.h": {
27
+ "size": 1930,
28
+ "sha256": "a25356630d3058f0a0c8937d9f297e9471e037fda58c2696d8a505c2bd99cb00"
29
+ },
30
+ "js_native_api.h": {
31
+ "size": 31081,
32
+ "sha256": "0953395662ea63697cd7a77a9320d94aa2829f9c9569abf3a74272b7a2923ce8"
33
+ },
34
+ "js_native_api_types.h": {
35
+ "size": 6896,
36
+ "sha256": "e785c16a9ce200e801242275cfd59090b029b5dae309a1a258f48d1570bf35ab"
37
+ }
38
+ }
39
+ },
40
+ "compiler": {
41
+ "path": "/usr/bin/cc",
42
+ "sha256": "1b99826121ae6682a634e5efe09bd3e3df58ce58e0b28f849114ab5b89139c26",
43
+ "version": "x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0\nCopyright (C) 2023 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n"
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,3 @@
1
+ import type { NativeSeekBinding } from "#safe-fs-native-seek";
2
+ export declare function loadNativeSeekBinding(signal?: AbortSignal): Promise<NativeSeekBinding>;
3
+ export declare function callNativeSeekEnd(binding: NativeSeekBinding, seek: NativeSeekBinding["seekEnd"], descriptor: number): Promise<bigint>;
@@ -0,0 +1,54 @@
1
+ import { getSystemErrorName } from "node:util";
2
+ import { FsError, isErrnoCode } from "../contracts/errors.js";
3
+ export async function loadNativeSeekBinding(signal) {
4
+ signal?.throwIfAborted();
5
+ const initialization = (async () => {
6
+ const module = await import("#safe-fs-native-seek");
7
+ signal?.throwIfAborted();
8
+ const load = module.loadBinding;
9
+ signal?.throwIfAborted();
10
+ if (typeof load !== "function")
11
+ throw new FsError("EIO");
12
+ const binding = await Reflect.apply(load, module, []);
13
+ signal?.throwIfAborted();
14
+ if (binding === null || typeof binding !== "object" || Array.isArray(binding))
15
+ throw new FsError("EIO");
16
+ return binding;
17
+ })();
18
+ if (!signal)
19
+ return initialization;
20
+ return new Promise((resolve, reject) => {
21
+ const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
22
+ initialization.then(binding => {
23
+ signal.removeEventListener("abort", abort);
24
+ if (signal.aborted)
25
+ reject(signal.reason);
26
+ else
27
+ resolve(binding);
28
+ }, error => {
29
+ signal.removeEventListener("abort", abort);
30
+ reject(signal.aborted ? signal.reason : error);
31
+ });
32
+ signal.addEventListener("abort", abort, { once: true });
33
+ if (signal.aborted)
34
+ abort();
35
+ });
36
+ }
37
+ export async function callNativeSeekEnd(binding, seek, descriptor) {
38
+ const result = await Reflect.apply(seek, binding, [descriptor]);
39
+ if (result === null || typeof result !== "object" || Array.isArray(result))
40
+ throw new FsError("EIO");
41
+ const { offset, errno } = result;
42
+ if (typeof offset !== "bigint" || typeof errno !== "number" || !Number.isSafeInteger(errno)
43
+ || errno < 0 || errno > 2147483647)
44
+ throw new FsError("EIO");
45
+ if (errno !== 0) {
46
+ if (offset !== -1n)
47
+ throw new FsError("EIO");
48
+ const code = getSystemErrorName(-errno);
49
+ throw new FsError(isErrnoCode(code) ? code : "EIO");
50
+ }
51
+ if (offset < 0n || offset > 9223372036854775807n)
52
+ throw new FsError("EIO");
53
+ return offset;
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.511",
3
+ "version": "0.1.513",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -107,6 +107,12 @@
107
107
  },
108
108
  "browser": "./dist/safe-fs/platform/browser.js",
109
109
  "default": "./dist/safe-fs/platform/node.js"
110
+ },
111
+ "#safe-fs-native-seek": {
112
+ "types": "./dist/safe-fs/native/fs-seek/loader.d.ts",
113
+ "workerd": null,
114
+ "browser": null,
115
+ "default": "./dist/safe-fs/native/fs-seek/loader.mjs"
110
116
  }
111
117
  }
112
118
  }