@poe-platform/safe-fs 0.1.721 → 0.1.723

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 (58) hide show
  1. package/README.md +67 -21
  2. package/dist/safe-fs/bridge/filesystem.d.ts +4 -0
  3. package/dist/safe-fs/bridge/filesystem.js +49 -10
  4. package/dist/safe-fs/bridge/types.d.ts +6 -0
  5. package/dist/safe-fs/contracts/descriptor.d.ts +5 -0
  6. package/dist/safe-fs/contracts/filesystem.d.ts +15 -0
  7. package/dist/safe-fs/contracts/io.d.ts +3 -1
  8. package/dist/safe-fs/contracts/io.js +48 -16
  9. package/dist/safe-fs/core.d.ts +2 -1
  10. package/dist/safe-fs/core.js +1 -1
  11. package/dist/safe-fs/fs/capabilities.js +6 -3
  12. package/dist/safe-fs/fs/descriptor.js +9 -2
  13. package/dist/safe-fs/fs/devices/index.d.ts +1 -0
  14. package/dist/safe-fs/fs/devices/index.js +24 -6
  15. package/dist/safe-fs/fs/memory/index.d.ts +2 -0
  16. package/dist/safe-fs/fs/memory/index.js +95 -4
  17. package/dist/safe-fs/fs/memory/limits.js +6 -4
  18. package/dist/safe-fs/fs/mount/index.d.ts +4 -0
  19. package/dist/safe-fs/fs/mount/index.js +76 -12
  20. package/dist/safe-fs/fs/object-publication/index.d.ts +4 -0
  21. package/dist/safe-fs/fs/object-publication/index.js +9 -8
  22. package/dist/safe-fs/fs/overlay/index.d.ts +4 -2
  23. package/dist/safe-fs/fs/overlay/index.js +92 -36
  24. package/dist/safe-fs/fs/quota/index.js +83 -23
  25. package/dist/safe-fs/fs/readonly/index.js +5 -3
  26. package/dist/safe-fs/fs/real/index.d.ts +21 -1
  27. package/dist/safe-fs/fs/real/index.js +266 -22
  28. package/dist/safe-fs/fs/s3/filesystem.d.ts +8 -0
  29. package/dist/safe-fs/fs/s3/filesystem.js +59 -12
  30. package/dist/safe-fs/fs/s3/http/request.d.ts +1 -1
  31. package/dist/safe-fs/fs/s3/http/request.js +1 -1
  32. package/dist/safe-fs/fs/s3/http/transport.js +8 -6
  33. package/dist/safe-fs/fs/s3/http/xml.d.ts +4 -1
  34. package/dist/safe-fs/fs/s3/http/xml.js +6 -2
  35. package/dist/safe-fs/fs/s3/manifest-admission.d.ts +8 -0
  36. package/dist/safe-fs/fs/s3/manifest-admission.js +171 -0
  37. package/dist/safe-fs/fs/s3/namespace.js +13 -10
  38. package/dist/safe-fs/fs/scoped.d.ts +1 -0
  39. package/dist/safe-fs/fs/scoped.js +54 -35
  40. package/dist/safe-fs/fs/webdav/webdav.d.ts +4 -1
  41. package/dist/safe-fs/fs/webdav/webdav.js +101 -23
  42. package/dist/safe-fs/node/filesystem.d.ts +8 -1
  43. package/dist/safe-fs/node/filesystem.js +7 -1
  44. package/dist/safe-fs/node/host.d.ts +3 -0
  45. package/dist/safe-fs/node/host.js +78 -0
  46. package/dist/safe-fs/node/index.d.ts +2 -1
  47. package/dist/safe-fs/node/index.js +2 -1
  48. package/dist/safe-fs/platform/browser.d.ts +1 -0
  49. package/dist/safe-fs/platform/browser.js +1 -0
  50. package/dist/safe-fs/platform/node.d.ts +1 -0
  51. package/dist/safe-fs/platform/node.js +1 -0
  52. package/dist/safe-fs/python/emscripten.d.ts +1 -0
  53. package/dist/safe-fs/python/emscripten.js +5 -5
  54. package/dist/safe-fs/python/filesystem.js +5 -5
  55. package/dist/safe-fs/python/native.d.ts +1 -0
  56. package/dist/safe-fs/python/native.js +3 -2
  57. package/dist/safe-fs/xml.js +32 -21
  58. package/package.json +1 -1
@@ -3,8 +3,12 @@ import { randomBytes } from "node:crypto";
3
3
  import { posix } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { FileSystemBridge } from "../bridge/filesystem.js";
6
+ const providers = new WeakMap();
7
+ export function getNodeFsBridgeProvider(bridge) {
8
+ return providers.get(bridge);
9
+ }
6
10
  export function createNodeFsBridge(fs, options = {}) {
7
- return new FileSystemBridge(fs, options, {
11
+ const bridge = new FileSystemBridge(fs, options, {
8
12
  codec: {
9
13
  isEncoding: Buffer.isEncoding,
10
14
  encode(text, encoding) {
@@ -25,4 +29,6 @@ export function createNodeFsBridge(fs, options = {}) {
25
29
  randomSuffix() { return randomBytes(6).toString("hex").slice(0, 6); },
26
30
  paths: posix
27
31
  });
32
+ providers.set(bridge, fs);
33
+ return bridge;
28
34
  }
@@ -0,0 +1,3 @@
1
+ import type { FileSystem } from "../contracts/filesystem.js";
2
+ /** Trusted host access with native paths and symlink semantics; this is not a rooted sandbox. */
3
+ export declare function createHostFileSystem(): FileSystem;
@@ -0,0 +1,78 @@
1
+ import * as fs from "node:fs/promises";
2
+ function snapshot(stat) {
3
+ return {
4
+ type: stat.isDirectory()
5
+ ? "directory"
6
+ : stat.isSymbolicLink()
7
+ ? "symlink"
8
+ : stat.isFile()
9
+ ? "file"
10
+ : "character",
11
+ size: stat.size,
12
+ mode: stat.mode,
13
+ mtimeMs: stat.mtimeMs,
14
+ atimeMs: stat.atimeMs,
15
+ ctimeMs: stat.ctimeMs,
16
+ birthtimeMs: stat.birthtimeMs
17
+ };
18
+ }
19
+ /** Trusted host access with native paths and symlink semantics; this is not a rooted sandbox. */
20
+ export function createHostFileSystem() {
21
+ async function perform(options, action) {
22
+ options?.signal?.throwIfAborted();
23
+ const result = await action();
24
+ options?.signal?.throwIfAborted();
25
+ return result;
26
+ }
27
+ return {
28
+ capabilities: {
29
+ read: true,
30
+ write: true,
31
+ append: true,
32
+ stat: true,
33
+ readdir: true,
34
+ mkdir: true,
35
+ recursiveMkdir: true,
36
+ rename: true,
37
+ remove: true,
38
+ removeDirectory: true,
39
+ recursiveRemove: true,
40
+ symlinks: true,
41
+ hardlinks: true,
42
+ permissions: true,
43
+ timestamps: true,
44
+ exclusiveCreate: true
45
+ },
46
+ readFile: (path, options) => perform(options, () => fs.readFile(path, options?.signal ? { signal: options.signal } : undefined)),
47
+ writeFile: (path, data, options) => perform(options, () => fs.writeFile(path, data, options)),
48
+ appendFile: (path, data, options) => perform(options, () => fs.appendFile(path, data, options)),
49
+ stat: (path, options) => perform(options, async () => snapshot(await fs.stat(path))),
50
+ lstat: (path, options) => perform(options, async () => snapshot(await fs.lstat(path))),
51
+ readdir: (path, options) => perform(options, async () => (await fs.readdir(path, { withFileTypes: true })).map((entry) => ({
52
+ name: entry.name,
53
+ type: entry.isDirectory()
54
+ ? "directory"
55
+ : entry.isSymbolicLink()
56
+ ? "symlink"
57
+ : entry.isFile()
58
+ ? "file"
59
+ : "character"
60
+ }))),
61
+ mkdir: (path, options) => perform(options, async () => {
62
+ await fs.mkdir(path, options);
63
+ }),
64
+ rm: (path, options) => perform(options, () => fs.rm(path, options)),
65
+ unlink: (path, options) => perform(options, () => fs.unlink(path)),
66
+ rmdir: (path, options) => perform(options, () => fs.rmdir(path)),
67
+ rename: (source, destination, options) => perform(options, () => fs.rename(source, destination)),
68
+ copyFile: (source, destination, options) => perform(options, () => fs.copyFile(source, destination, options?.exclusive ? fs.constants.COPYFILE_EXCL : 0)),
69
+ realpath: (path, options) => perform(options, () => fs.realpath(path)),
70
+ access: (path, mode, options) => perform(options, () => fs.access(path, mode)),
71
+ readlink: (path, options) => perform(options, () => fs.readlink(path)),
72
+ symlink: (target, path, options) => perform(options, () => fs.symlink(target, path)),
73
+ link: (source, destination, options) => perform(options, () => fs.link(source, destination)),
74
+ chmod: (path, mode, options) => perform(options, () => fs.chmod(path, mode)),
75
+ utimes: (path, atimeMs, mtimeMs, options) => perform(options, () => fs.utimes(path, atimeMs / 1000, mtimeMs / 1000)),
76
+ truncate: (path, length, options) => perform(options, () => fs.truncate(path, length))
77
+ };
78
+ }
@@ -1,2 +1,3 @@
1
- export { createNodeFsBridge } from "./filesystem.js";
1
+ export { createNodeFsBridge, getNodeFsBridgeProvider } from "./filesystem.js";
2
2
  export type { NodeFsBridgeOptions, NodeFsBridgeFileSystem, NodeFsImplementation } from "./filesystem.js";
3
+ export { createHostFileSystem } from "./host.js";
@@ -1 +1,2 @@
1
- export { createNodeFsBridge } from "./filesystem.js";
1
+ export { createNodeFsBridge, getNodeFsBridgeProvider } from "./filesystem.js";
2
+ export { createHostFileSystem } from "./host.js";
@@ -2,6 +2,7 @@ import type { FsOptions } from "../contracts/filesystem.js";
2
2
  export type PlatformErrno = number | undefined;
3
3
  export type PlatformComparisonCallback<Callback> = Callback & never;
4
4
  export declare const platform: Readonly<{
5
+ maxCollectionBytes: number;
5
6
  errno(_code: string): PlatformErrno;
6
7
  callbackAuthorities: false;
7
8
  randomUUID(): string;
@@ -1,5 +1,6 @@
1
1
  import { FsError } from "../contracts/errors.js";
2
2
  export const platform = Object.freeze({
3
+ maxCollectionBytes: 32 * 1024 * 1024,
3
4
  errno(_code) {
4
5
  return undefined;
5
6
  },
@@ -3,6 +3,7 @@ import type { FsOptions } from "../contracts/filesystem.js";
3
3
  export type PlatformErrno = number;
4
4
  export type PlatformComparisonCallback<Callback> = Callback;
5
5
  export declare const platform: Readonly<{
6
+ maxCollectionBytes: number;
6
7
  errno(code: string): PlatformErrno;
7
8
  callbackAuthorities: true;
8
9
  randomUUID: typeof randomUUID;
@@ -15,6 +15,7 @@ const systemErrnos = (() => {
15
15
  })();
16
16
  const negotiating = new AsyncLocalStorage();
17
17
  export const platform = Object.freeze({
18
+ maxCollectionBytes: Infinity,
18
19
  errno(code) {
19
20
  const errno = systemErrnos.get(code === "EOPNOTSUPP" ? "ENOTSUP" : code);
20
21
  if (errno === undefined)
@@ -6,6 +6,7 @@ export interface PythonMountOptions {
6
6
  errno: Record<string, number>;
7
7
  synchronizationFlags: number;
8
8
  runtimeModule: any;
9
+ getUmask?: () => number;
9
10
  }
10
11
  export declare function mountPythonFileSystem(FS: any, options: PythonMountOptions): void;
11
12
  /** Encode the pinned wasm32 musl stat layout without truncating canonical values. */
@@ -59,9 +59,9 @@ export function mountPythonFileSystem(FS, options) {
59
59
  mknod(parent, name, mode) {
60
60
  const target = `${path(parent)}/${name}`;
61
61
  if (FS.isDir(mode))
62
- rpc('mkdir', target, { mode: mode & 4095 });
62
+ rpc('mkdir', target, { mode: mode & 4095 & ~(options.getUmask?.() ?? 0), exactMode: true });
63
63
  else {
64
- const handle = rpc('open', target, { access: 'write', creation: 'exclusive', mode: mode & 4095 });
64
+ const handle = rpc('open', target, { access: 'write', creation: 'exclusive', mode: mode & 4095 & ~(options.getUmask?.() ?? 0), exactMode: true });
65
65
  rpc('close', handle);
66
66
  }
67
67
  return node(parent, name, rpc('lstat', target));
@@ -145,7 +145,7 @@ export function mountPythonFileSystem(FS, options) {
145
145
  throw new FS.ErrnoError(54);
146
146
  let openOptions;
147
147
  try {
148
- openOptions = translatePythonOpenFlags(flags, mode);
148
+ openOptions = { ...translatePythonOpenFlags(flags, mode & ~(options.getUmask?.() ?? 0)), exactMode: true };
149
149
  }
150
150
  catch (error) {
151
151
  throw new FS.ErrnoError(errno[error.code] ?? errno.EIO);
@@ -199,7 +199,7 @@ export function mountPythonFileSystem(FS, options) {
199
199
  if (bootstrapPath(target))
200
200
  return originalMkdir(target, mode);
201
201
  const absolute = absolutePath(target);
202
- rpc('mkdir', absolute, { mode });
202
+ rpc('mkdir', absolute, { mode: mode & ~(options.getUmask?.() ?? 0), exactMode: true });
203
203
  return node(FS.root, absolute.slice(1), rpc('lstat', absolute));
204
204
  };
205
205
  const originalRename = FS.rename.bind(FS);
@@ -238,7 +238,7 @@ export function mountPythonFileSystem(FS, options) {
238
238
  return originalMknod(target, mode, device);
239
239
  if (!FS.isFile(mode))
240
240
  throw new FS.ErrnoError(errno.ENOTSUP);
241
- const handle = rpc('open', absolutePath(target), { access: 'write', creation: 'exclusive', mode: mode & 4095 });
241
+ const handle = rpc('open', absolutePath(target), { access: 'write', creation: 'exclusive', mode: mode & 4095 & ~(options.getUmask?.() ?? 0), exactMode: true });
242
242
  rpc('close', handle);
243
243
  return node(FS.root, absolutePath(target).slice(1), rpc('lstat', absolutePath(target)));
244
244
  };
@@ -24,10 +24,10 @@ export class PythonFileSystem {
24
24
  throw new FsError("EINVAL", { syscall: "python filesystem", path: options.cwd, message: "cwd must be absolute" });
25
25
  this.#cwd = options.cwd;
26
26
  this.#transfer = options.maxTransferBytes ?? 65536;
27
- this.#limit = options.maxOpenFiles ?? 256;
28
- this.#directoryLimit = options.maxDirectoryEntries ?? 65536;
29
- for (const limit of [this.#transfer, this.#limit, this.#directoryLimit])
30
- if (!Number.isSafeInteger(limit) || limit < 1)
27
+ this.#limit = options.maxOpenFiles ?? Infinity;
28
+ this.#directoryLimit = options.maxDirectoryEntries ?? Infinity;
29
+ for (const limit of [this.#transfer, options.maxOpenFiles, options.maxDirectoryEntries])
30
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1))
31
31
  throw new FsError("EINVAL", { syscall: "python filesystem" });
32
32
  this.#open = options.open;
33
33
  this.#scope = composeAbortSignals([...(options.signal === undefined ? [] : [options.signal]), this.#abort.signal]);
@@ -153,7 +153,7 @@ export class PythonFileSystem {
153
153
  case "realpath": return fs.realpath(this.#path(request.args[0]), options);
154
154
  case "readdir": {
155
155
  const path = this.#path(request.args[0]);
156
- const entries = await fs.readdir(path, { ...options, maxEntries: this.#directoryLimit });
156
+ const entries = await fs.readdir(path, { ...options, ...(this.#directoryLimit === Infinity ? {} : { maxEntries: this.#directoryLimit }) });
157
157
  signal.throwIfAborted();
158
158
  if (entries.length > this.#directoryLimit)
159
159
  throw new FsError("EFBIG", { syscall: "readdir", path, message: "Python directory listing exceeds configured limit" });
@@ -8,6 +8,7 @@ export interface PythonNativeSyscallOptions {
8
8
  readonly cwd: string;
9
9
  readonly runtimeMount: string;
10
10
  readonly maxTransferBytes: number;
11
+ readonly getUmask?: () => number;
11
12
  readonly original?: (name: string, args: readonly (number | bigint)[]) => number;
12
13
  }
13
14
  export declare function createPythonNativeSyscalls(options: PythonNativeSyscallOptions): {
@@ -195,7 +195,8 @@ export function createPythonNativeSyscalls(options) {
195
195
  }
196
196
  if (fourth)
197
197
  range(fourth, 4);
198
- const openOptions = translatePythonOpenFlags(third, fourth ? view().getUint32(fourth, true) : 0o666);
198
+ const mode = (fourth ? view().getUint32(fourth, true) : 0o666) & ~(options.getUmask?.() ?? 0);
199
+ const openOptions = { ...translatePythonOpenFlags(third, mode), exactMode: true };
199
200
  const handle = await options.dispatch({ op: 'open', args: [path, openOptions] });
200
201
  try {
201
202
  signal.throwIfAborted();
@@ -338,7 +339,7 @@ export function createPythonNativeSyscalls(options) {
338
339
  if (privatePath(path))
339
340
  fail('EROFS');
340
341
  if (name === '__syscall_mkdirat')
341
- await request('mkdir', path, { mode: third & 0o7777 });
342
+ await request('mkdir', path, { mode: third & 0o7777 & ~(options.getUmask?.() ?? 0), exactMode: true });
342
343
  else {
343
344
  if (third !== 0 && third !== 512)
344
345
  fail('EINVAL');
@@ -185,22 +185,22 @@ function* validDeclaration(content, expectedEncoding) {
185
185
  return offset === content.length;
186
186
  }
187
187
  export function* parseXmlSteps(input, limits = {}) {
188
- const maxDepth = limits.maxDepth ?? 64;
189
- const maxNodes = limits.maxNodes ?? 100_000;
190
- const maxAttributes = limits.maxAttributes ?? 10_000;
188
+ const maxDepth = limits.maxDepth ?? Infinity;
189
+ const maxNodes = limits.maxNodes ?? Infinity;
190
+ const maxAttributes = limits.maxAttributes ?? Infinity;
191
191
  const retainContent = limits.retainContent !== false;
192
- const maxContentNodes = limits.maxContentNodes ?? (retainContent ? 100_000 : maxNodes);
192
+ const maxContentNodes = limits.maxContentNodes ?? Infinity;
193
193
  const emptyContent = Object.freeze([]);
194
194
  const emptyAttributes = Object.freeze([]);
195
195
  const emptyNamespaces = new Map();
196
- const maxAttributesPerElement = limits.maxAttributesPerElement ?? 128;
197
- const maxNamespaces = limits.maxNamespaces ?? 256;
198
- for (const limit of [maxDepth, maxNodes, maxAttributes, maxContentNodes, maxAttributesPerElement, maxNamespaces]) {
199
- if (!Number.isSafeInteger(limit) || limit < 1)
196
+ const maxAttributesPerElement = limits.maxAttributesPerElement ?? Infinity;
197
+ const maxNamespaces = limits.maxNamespaces ?? Infinity;
198
+ for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
199
+ if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
200
200
  throw new RangeError("XML limits must be positive integers");
201
201
  }
202
- const maxTextLength = limits.maxTextLength ?? Number.MAX_SAFE_INTEGER;
203
- if (!Number.isSafeInteger(maxTextLength) || maxTextLength < 1)
202
+ const maxTextLength = limits.maxTextLength ?? Infinity;
203
+ if (limits.maxTextLength !== undefined && ((maxTextLength !== Infinity && !Number.isSafeInteger(maxTextLength)) || maxTextLength < 1))
204
204
  throw new RangeError("XML limits must be positive integers");
205
205
  let textLength = 0;
206
206
  const admitText = (text) => {
@@ -209,31 +209,42 @@ export function* parseXmlSteps(input, limits = {}) {
209
209
  textLength += text.length;
210
210
  };
211
211
  const chunks = [];
212
- let chunk = "";
213
- for (let index = input.charCodeAt(0) === 0xfeff ? 1 : 0; index < input.length; index++) {
212
+ const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
213
+ let chunkStart = start;
214
+ let chunkLength = 0;
215
+ let normalizedChunk = "";
216
+ // Keep validated spans intact; only changed line endings need new strings.
217
+ for (let index = start; index < input.length; index++) {
214
218
  const point = input.codePointAt(index);
215
219
  if (!validCharacter(point))
216
220
  invalid("invalid character");
217
221
  if (point === 13) {
218
- chunk += "\n";
222
+ normalizedChunk += input.slice(chunkStart, index) + "\n";
219
223
  if (input.charCodeAt(index + 1) === 10)
220
224
  index++;
225
+ chunkStart = index + 1;
226
+ chunkLength++;
221
227
  }
222
228
  else {
223
- chunk += String.fromCodePoint(point);
229
+ chunkLength += point > 0xffff ? 2 : 1;
224
230
  if (point > 0xffff)
225
231
  index++;
226
232
  }
227
- if (chunk.length >= 512) {
228
- chunks.push(chunk);
229
- chunk = "";
233
+ if (chunkLength >= 512) {
234
+ if (normalizedChunk) {
235
+ chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
236
+ normalizedChunk = "";
237
+ chunkStart = index + 1;
238
+ }
239
+ chunkLength = 0;
230
240
  yield 512;
231
241
  }
232
242
  }
233
- chunks.push(chunk);
234
- if (chunk.length)
235
- yield chunk.length;
236
- const source = chunks.join("");
243
+ if (chunkLength)
244
+ yield chunkLength;
245
+ if (chunks.length || normalizedChunk)
246
+ chunks.push(normalizedChunk + input.slice(chunkStart));
247
+ const source = chunks.length ? chunks.join("") : input.slice(start);
237
248
  const stack = [];
238
249
  let root;
239
250
  const prolog = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.721",
3
+ "version": "0.1.723",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",