@openclaw/fs-safe 0.10.0 → 0.12.0
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.
- package/CHANGELOG.md +114 -0
- package/LICENSE +1 -0
- package/README.md +39 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -1
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +8 -8
- package/dist/archive-gzip-tail.d.ts +1 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +16 -9
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -12
- package/dist/archive-native.js +4 -4
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +54 -42
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +7 -6
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +16 -13
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +26 -9
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +5 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +18 -11
- package/dist/copy-file-input.d.ts +0 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +4 -26
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +57 -26
- package/dist/copy.d.ts +1 -1
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +3 -1
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +99 -32
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +15 -5
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +32 -14
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +52 -16
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +421 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +16 -2
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +43 -84
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -1
- package/dist/move-path-stage.d.ts.map +1 -1
- package/dist/move-path-stage.js +2 -1
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +4 -3
- package/dist/mutation-authority.d.ts +1 -0
- package/dist/mutation-authority.d.ts.map +1 -1
- package/dist/mutation-authority.js +4 -4
- package/dist/native-binding.d.ts +14 -2
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +3 -2
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +2 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +22 -3
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +48 -6
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +40 -13
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +3 -1
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +31 -5
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +5 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +13 -12
- package/dist/root-impl.d.ts +13 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +93 -70
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +14 -5
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +24 -32
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +2 -1
- package/dist/root-symlink-policy.d.ts.map +1 -1
- package/dist/root-symlink-policy.js +2 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +21 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +27 -7
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +2 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +49 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +8 -5
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +16 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +17 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -7
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +7 -5
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +14 -12
- package/dist/write-file-handle.d.ts +1 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +3 -2
- package/docs/advanced.md +7 -1
- package/docs/archive.md +31 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +33 -2
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +40 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +21 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +146 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +34 -0
- package/docs/native-helper.md +14 -5
- package/docs/native.md +18 -2
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/path.md +1 -1
- package/docs/permissions.md +37 -3
- package/docs/public-api.md +36 -2
- package/docs/root.md +33 -3
- package/docs/secure-file.md +17 -14
- package/docs/security-model.md +1 -1
- package/docs/sidecar-lock.md +12 -3
- package/docs/store.md +22 -1
- package/docs/temp.md +39 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
package/dist/sidecar-lock.js
CHANGED
|
@@ -37,9 +37,6 @@ function resolveManagerState(key) {
|
|
|
37
37
|
// Backfill state created by fs-safe versions that predate reclaim guards.
|
|
38
38
|
state.reclaimCleanupRegistered ??= false;
|
|
39
39
|
state.reclaimGuards ??= new Set();
|
|
40
|
-
for (const held of state.held.values()) {
|
|
41
|
-
held.refCount ??= 1;
|
|
42
|
-
}
|
|
43
40
|
}
|
|
44
41
|
return state;
|
|
45
42
|
}
|
|
@@ -163,6 +160,8 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
163
160
|
await held.releasePromise;
|
|
164
161
|
return true;
|
|
165
162
|
}
|
|
163
|
+
// Older package copies can add holders after this manager was constructed.
|
|
164
|
+
held.refCount ??= 1;
|
|
166
165
|
if (options.force) {
|
|
167
166
|
held.refCount = 0;
|
|
168
167
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,KAAK,iBAAiB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,OAAO,CAKT;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAYvF;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;CAC5B,CA+BA"}
|
package/dist/staged-directory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { realpathSync } from "./realpath.js";
|
|
4
5
|
export function exactIdentityMatches(expected, actual) {
|
|
5
6
|
return ["dev", "ino"].every((key) => {
|
|
6
7
|
const value = expected[key];
|
|
@@ -14,7 +15,7 @@ export function describeStagedDirectory(fd, pathname) {
|
|
|
14
15
|
}
|
|
15
16
|
const receipt = Object.freeze({
|
|
16
17
|
path: path.resolve(pathname),
|
|
17
|
-
realPath:
|
|
18
|
+
realPath: realpathSync(pathname),
|
|
18
19
|
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
19
20
|
});
|
|
20
21
|
assertStagedDirectoryCurrent(receipt);
|
|
@@ -23,7 +24,7 @@ export function describeStagedDirectory(fd, pathname) {
|
|
|
23
24
|
export function assertStagedDirectoryCurrent(receipt) {
|
|
24
25
|
const current = fs.lstatSync(receipt.path, { bigint: true });
|
|
25
26
|
if (!current.isDirectory() || !exactIdentityMatches(receipt.identity, current) ||
|
|
26
|
-
|
|
27
|
+
realpathSync(receipt.path) !== receipt.realPath) {
|
|
27
28
|
throw new FsSafeError("path-mismatch", "staging directory pathname changed");
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -37,7 +38,7 @@ export function openStagedDirectory(directory) {
|
|
|
37
38
|
if (!before.isDirectory()) {
|
|
38
39
|
throw new FsSafeError("not-file", "staging parent must be a real directory");
|
|
39
40
|
}
|
|
40
|
-
if (expected && (!exactIdentityMatches(expected, before) ||
|
|
41
|
+
if (expected && (!exactIdentityMatches(expected, before) || realpathSync(pathname) !== expected.realPath)) {
|
|
41
42
|
throw new FsSafeError("path-mismatch", "stale staging directory receipt");
|
|
42
43
|
}
|
|
43
44
|
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
package/dist/temp-target.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
1
2
|
export type TempFile = {
|
|
2
3
|
dir: string;
|
|
3
4
|
path: string;
|
|
@@ -5,6 +6,12 @@ export type TempFile = {
|
|
|
5
6
|
cleanup: () => Promise<void>;
|
|
6
7
|
[Symbol.asyncDispose](): Promise<void>;
|
|
7
8
|
};
|
|
9
|
+
type TempFileOptions = {
|
|
10
|
+
rootDir?: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
fileName?: string;
|
|
13
|
+
onCleanupError?: (error: unknown) => void;
|
|
14
|
+
};
|
|
8
15
|
export declare function sanitizeTempFileName(fileName: string): string;
|
|
9
16
|
export declare function buildRandomTempFilePath(params: {
|
|
10
17
|
rootDir?: string;
|
|
@@ -13,16 +20,11 @@ export declare function buildRandomTempFilePath(params: {
|
|
|
13
20
|
now?: number;
|
|
14
21
|
uuid?: string;
|
|
15
22
|
}): string;
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
rootDir?: string;
|
|
24
|
-
prefix: string;
|
|
25
|
-
fileName?: string;
|
|
26
|
-
onCleanupError?: (error: unknown) => void;
|
|
27
|
-
}, fn: (tmpPath: string) => Promise<T>): Promise<T>;
|
|
23
|
+
export declare function createOwnedTempFile(params: TempFileOptions): Promise<{
|
|
24
|
+
target: TempFile;
|
|
25
|
+
identity: Readonly<Pick<fsSync.BigIntStats, "dev" | "ino">>;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function tempFile(params: TempFileOptions): Promise<TempFile>;
|
|
28
|
+
export declare function withTempFile<T>(params: TempFileOptions, fn: (tmpPath: string) => Promise<T>): Promise<T>;
|
|
29
|
+
export {};
|
|
28
30
|
//# sourceMappingURL=temp-target.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAS7B,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyCD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IAC1E,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAC7D,CAAC,CA2BD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
package/dist/temp-target.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { suffixWindowsReservedDeviceName } from "./filename.js";
|
|
5
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
7
|
import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
|
|
7
8
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -57,9 +58,10 @@ function sanitizeExtension(extension) {
|
|
|
57
58
|
return token ? `.${token}` : "";
|
|
58
59
|
}
|
|
59
60
|
export function sanitizeTempFileName(fileName) {
|
|
60
|
-
|
|
61
|
+
const sanitized = sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
|
|
61
62
|
allowDotPrefix: true,
|
|
62
63
|
});
|
|
64
|
+
return suffixWindowsReservedDeviceName(sanitized);
|
|
63
65
|
}
|
|
64
66
|
export function buildRandomTempFilePath(params) {
|
|
65
67
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
@@ -106,7 +108,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
|
106
108
|
function resolveTempRoot(rootDir) {
|
|
107
109
|
return path.resolve(rootDir ?? resolveSecureTempRoot({ fallbackPrefix: "fs-safe" }));
|
|
108
110
|
}
|
|
109
|
-
export async function
|
|
111
|
+
export async function createOwnedTempFile(params) {
|
|
110
112
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
111
113
|
const prefix = `${sanitizePrefix(params.prefix)}-`;
|
|
112
114
|
const dir = await fs.mkdtemp(path.join(rootDir, prefix));
|
|
@@ -124,13 +126,19 @@ export async function tempFile(params) {
|
|
|
124
126
|
}
|
|
125
127
|
};
|
|
126
128
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
target: {
|
|
130
|
+
dir,
|
|
131
|
+
path: file(),
|
|
132
|
+
file,
|
|
133
|
+
cleanup,
|
|
134
|
+
[Symbol.asyncDispose]: cleanup,
|
|
135
|
+
},
|
|
136
|
+
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
132
137
|
};
|
|
133
138
|
}
|
|
139
|
+
export async function tempFile(params) {
|
|
140
|
+
return (await createOwnedTempFile(params)).target;
|
|
141
|
+
}
|
|
134
142
|
export async function withTempFile(params, fn) {
|
|
135
143
|
const target = await tempFile(params);
|
|
136
144
|
try {
|
package/dist/trash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAkLF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|
package/dist/trash.js
CHANGED
|
@@ -3,6 +3,8 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
5
|
import { guardedRenameSync, guardedRmSync } from "./guarded-mutation.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
7
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
6
8
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
7
9
|
const TRASH_DESTINATION_COLLISION_CODES = new Set(["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"]);
|
|
8
10
|
const TRASH_DESTINATION_RETRY_LIMIT = 4;
|
|
@@ -26,7 +28,7 @@ function resolveAllowedTrashRoots(allowedRoots) {
|
|
|
26
28
|
try {
|
|
27
29
|
// Keep both spellings: broken symlink targets cannot be realpathed and
|
|
28
30
|
// may only compare equal to the caller's lexical allowed root.
|
|
29
|
-
return [path.resolve(
|
|
31
|
+
return [path.resolve(realpathSync.native(root)), lexicalRoot];
|
|
30
32
|
}
|
|
31
33
|
catch {
|
|
32
34
|
return [lexicalRoot];
|
|
@@ -36,7 +38,7 @@ function resolveAllowedTrashRoots(allowedRoots) {
|
|
|
36
38
|
}
|
|
37
39
|
function resolveTrashTargetPath(targetPath) {
|
|
38
40
|
try {
|
|
39
|
-
return { path: path.resolve(
|
|
41
|
+
return { path: path.resolve(realpathSync.native(targetPath)), resolved: true };
|
|
40
42
|
}
|
|
41
43
|
catch {
|
|
42
44
|
// Broken symlinks are valid trash targets. Fall back to the lexical path,
|
|
@@ -75,13 +77,13 @@ function assertTrashTargetGuard(guard) {
|
|
|
75
77
|
function resolveTrashDir() {
|
|
76
78
|
const homeDir = os.homedir();
|
|
77
79
|
const trashDir = path.join(homeDir, ".Trash");
|
|
78
|
-
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
80
|
+
fs.mkdirSync(recursiveMkdirPath(trashDir), { recursive: true, mode: 0o700 });
|
|
79
81
|
const trashDirStat = fs.lstatSync(trashDir);
|
|
80
82
|
if (!trashDirStat.isDirectory() || trashDirStat.isSymbolicLink()) {
|
|
81
83
|
throw new Error(`Refusing to use non-directory/symlink trash directory: ${trashDir}`);
|
|
82
84
|
}
|
|
83
|
-
const realHome = path.resolve(
|
|
84
|
-
const resolvedTrashDir = path.resolve(
|
|
85
|
+
const realHome = path.resolve(realpathSync.native(homeDir));
|
|
86
|
+
const resolvedTrashDir = path.resolve(realpathSync.native(trashDir));
|
|
85
87
|
if (resolvedTrashDir === realHome || !isSameOrChildPath(resolvedTrashDir, realHome)) {
|
|
86
88
|
throw new Error(`Trash directory escaped home directory: ${trashDir}`);
|
|
87
89
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unicode-path.d.ts","sourceRoot":"","sources":["../src/unicode-path.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,UAAQ,GAAG,MAAM,CAUhF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const NON_ASCII = /[^\x00-\x7f]/;
|
|
2
|
+
export function lowerCaseNfc(value) {
|
|
3
|
+
return NON_ASCII.test(value) ? value.normalize("NFC").toLowerCase().normalize("NFC") : value.toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
export function maxNormalizedUtf8Bytes(value, includeRaw = false) {
|
|
6
|
+
const nfc = value.normalize("NFC");
|
|
7
|
+
const bytes = Buffer.byteLength(nfc, "utf8");
|
|
8
|
+
const maximum = includeRaw && nfc !== value ? Math.max(bytes, Buffer.byteLength(value, "utf8")) : bytes;
|
|
9
|
+
// ASCII NFC output also has identical NFD and one UTF-8 byte per code unit.
|
|
10
|
+
if (bytes === nfc.length)
|
|
11
|
+
return maximum;
|
|
12
|
+
return Math.max(maximum, Buffer.byteLength(value.normalize("NFD"), "utf8"));
|
|
13
|
+
}
|
package/dist/walk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA+EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CA0DjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CA0D1C"}
|
package/dist/walk.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { realpathSync } from "./realpath.js";
|
|
4
5
|
function validateWalkBudget(name, value) {
|
|
5
6
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
6
7
|
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
@@ -28,11 +29,10 @@ function shouldStop(result, options) {
|
|
|
28
29
|
}
|
|
29
30
|
function buildEntry(params) {
|
|
30
31
|
const fullPath = params.fullPath;
|
|
31
|
-
const relativePath = path.relative(params.rootDir, fullPath) || params.dirent.name;
|
|
32
32
|
return {
|
|
33
33
|
name: params.dirent.name,
|
|
34
34
|
path: fullPath,
|
|
35
|
-
relativePath,
|
|
35
|
+
relativePath: params.relativePath,
|
|
36
36
|
depth: params.depth,
|
|
37
37
|
kind: params.kind ?? kindForDirent(params.dirent),
|
|
38
38
|
dirent: params.dirent,
|
|
@@ -78,12 +78,12 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
78
78
|
failedDirs: [],
|
|
79
79
|
};
|
|
80
80
|
const visitedDirs = new Set();
|
|
81
|
-
function visit(dir, depth) {
|
|
81
|
+
function visit(dir, relativeDir, depth) {
|
|
82
82
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
83
83
|
return;
|
|
84
84
|
let realDir;
|
|
85
85
|
try {
|
|
86
|
-
realDir =
|
|
86
|
+
realDir = realpathSync(dir);
|
|
87
87
|
}
|
|
88
88
|
catch (error) {
|
|
89
89
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -110,20 +110,21 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
110
110
|
const kind = resolveKind(fullPath, dirent, symlinks);
|
|
111
111
|
if (!kind)
|
|
112
112
|
continue;
|
|
113
|
-
const
|
|
113
|
+
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
114
|
+
const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
|
|
114
115
|
if (options.include?.(entry) ?? true) {
|
|
115
116
|
result.entries.push(entry);
|
|
116
117
|
}
|
|
117
118
|
if (kind === "directory" &&
|
|
118
119
|
(options.maxDepth === undefined || depth < options.maxDepth) &&
|
|
119
120
|
(options.descend?.(entry) ?? true)) {
|
|
120
|
-
visit(fullPath, depth + 1);
|
|
121
|
+
visit(fullPath, relativePath, depth + 1);
|
|
121
122
|
if (result.truncated)
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
visit(root, 1);
|
|
127
|
+
visit(root, "", 1);
|
|
127
128
|
return result;
|
|
128
129
|
}
|
|
129
130
|
export async function walkDirectory(rootDir, options = {}) {
|
|
@@ -137,12 +138,12 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
137
138
|
failedDirs: [],
|
|
138
139
|
};
|
|
139
140
|
const visitedDirs = new Set();
|
|
140
|
-
async function visit(dir, depth) {
|
|
141
|
+
async function visit(dir, relativeDir, depth) {
|
|
141
142
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
142
143
|
return;
|
|
143
144
|
let realDir;
|
|
144
145
|
try {
|
|
145
|
-
realDir =
|
|
146
|
+
realDir = realpathSync.native(dir);
|
|
146
147
|
}
|
|
147
148
|
catch (error) {
|
|
148
149
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -169,19 +170,20 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
169
170
|
const kind = resolveKind(fullPath, dirent, symlinks);
|
|
170
171
|
if (!kind)
|
|
171
172
|
continue;
|
|
172
|
-
const
|
|
173
|
+
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
174
|
+
const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
|
|
173
175
|
if (options.include?.(entry) ?? true) {
|
|
174
176
|
result.entries.push(entry);
|
|
175
177
|
}
|
|
176
178
|
if (kind === "directory" &&
|
|
177
179
|
(options.maxDepth === undefined || depth < options.maxDepth) &&
|
|
178
180
|
(options.descend?.(entry) ?? true)) {
|
|
179
|
-
await visit(fullPath, depth + 1);
|
|
181
|
+
await visit(fullPath, relativePath, depth + 1);
|
|
180
182
|
if (result.truncated)
|
|
181
183
|
return;
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
|
-
await visit(root, 1);
|
|
187
|
+
await visit(root, "", 1);
|
|
186
188
|
return result;
|
|
187
189
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
export declare function writeAllToFile(target: FileHandle | number, data: string | Uint8Array, options?: {
|
|
3
3
|
encoding?: BufferEncoding;
|
|
4
|
+
position?: number;
|
|
4
5
|
assertBeforeMutation?: () => void;
|
|
5
6
|
}): Promise<void>;
|
|
6
7
|
//# sourceMappingURL=write-file-handle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-file-handle.d.ts","sourceRoot":"","sources":["../src/write-file-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,
|
|
1
|
+
{"version":3,"file":"write-file-handle.d.ts","sourceRoot":"","sources":["../src/write-file-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -6,17 +6,18 @@ export async function writeAllToFile(target, data, options = {}) {
|
|
|
6
6
|
let offset = 0;
|
|
7
7
|
while (offset < buffer.byteLength) {
|
|
8
8
|
const length = Math.min(WRITE_CHUNK_BYTES, buffer.byteLength - offset);
|
|
9
|
+
const position = options.position === undefined ? null : options.position + offset;
|
|
9
10
|
options.assertBeforeMutation?.();
|
|
10
11
|
const written = typeof target === "number"
|
|
11
12
|
? await new Promise((resolve, reject) => {
|
|
12
|
-
fs.write(target, buffer, offset, length,
|
|
13
|
+
fs.write(target, buffer, offset, length, position, (error, bytesWritten) => {
|
|
13
14
|
if (error)
|
|
14
15
|
reject(error);
|
|
15
16
|
else
|
|
16
17
|
resolve(bytesWritten);
|
|
17
18
|
});
|
|
18
19
|
})
|
|
19
|
-
: (await target.write(buffer, offset, length,
|
|
20
|
+
: (await target.write(buffer, offset, length, position)).bytesWritten;
|
|
20
21
|
if (written <= 0) {
|
|
21
22
|
throw new FsSafeError("helper-failed", "file write made no progress");
|
|
22
23
|
}
|
package/docs/advanced.md
CHANGED
|
@@ -32,6 +32,7 @@ The exports group into a handful of themes. Each documented helper has its own p
|
|
|
32
32
|
| `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
|
|
33
33
|
| `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
|
|
34
34
|
| `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
|
|
35
|
+
| `probePathCaseInsensitiveSync`, `ProbePathCaseOptions` | [path-case.md](path-case.md) | Observe local ASCII-case behavior with explicit read-only mode and owned temporary-probe cleanup. |
|
|
35
36
|
|
|
36
37
|
`ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
|
|
37
38
|
returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
|
|
@@ -66,9 +67,12 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
66
67
|
|---|---|---|
|
|
67
68
|
| `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
|
|
68
69
|
| `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
|
|
70
|
+
| `copyFileHandle`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular-file handles from position zero with byte limits, settled cancellation, and a synchronous source observer; preserves cursors and leaves publication and cleanup to the caller. |
|
|
71
|
+
| `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
|
|
69
72
|
| `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
|
|
70
73
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
|
71
74
|
| `sameFileIdentity`, `FileIdentityStat` | – | Compare two stats for same-inode equality. |
|
|
75
|
+
| `readDirectoryIdentity`, `assertDirectoryIdentitySync`, `DirectoryIdentity` | [directory-identity.md](directory-identity.md) | Observe exact bigint directory identity and synchronously check a caller-selected path, optionally retaining its canonical path. |
|
|
72
76
|
| `pathExists`, `pathExistsSync` | – | Boolean existence check that does not throw on `ENOENT`. |
|
|
73
77
|
| `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
|
|
74
78
|
| `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
|
|
@@ -92,6 +96,8 @@ byte limit. Continuation buffers grow only after filling with actual bytes,
|
|
|
92
96
|
up to the byte budget plus its probe; file-size hints cannot force that growth.
|
|
93
97
|
Unknown-size inputs start with at most 64 KiB. This avoids per-chunk copies and
|
|
94
98
|
a final concatenation when a file exceeds the initial allocation.
|
|
99
|
+
Regular files that report a size of zero, such as virtual files, continue through
|
|
100
|
+
positive short reads until actual EOF or byte-limit overflow.
|
|
95
101
|
|
|
96
102
|
The bounded descriptor helpers start at the descriptor's current offset and
|
|
97
103
|
leave ownership with the caller. They are intended for the second half of a
|
|
@@ -129,7 +135,7 @@ component is followed by another segment, both helpers throw
|
|
|
129
135
|
|
|
130
136
|
| Export | Page | Notes |
|
|
131
137
|
|---|---|---|
|
|
132
|
-
| `safeDirName`, `safePathSegmentHashed`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories
|
|
138
|
+
| `safeDirName`, `safePathSegmentHashed`, `safePathSegmentHashedV2`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories; use V2 for untrusted identifier mappings. |
|
|
133
139
|
| `sanitizeUntrustedFileName` | [filename.md](filename.md) | Coerce an untrusted string into a safe filename. |
|
|
134
140
|
| `resolveHomeRelativePath` | – | Expand a leading `~` before resolving `.` and `..`; tildes inside relative paths stay literal. |
|
|
135
141
|
|
package/docs/archive.md
CHANGED
|
@@ -126,6 +126,12 @@ untrusted authority rejects explicitly instead of silently accepting a wrong
|
|
|
126
126
|
mode. Other unsupported search-only routes also fail closed. Windows retains
|
|
127
127
|
its existing bounded lack of POSIX mode enforcement.
|
|
128
128
|
|
|
129
|
+
Extraction and TAR inspection first copy the admitted source into a private
|
|
130
|
+
staging file. This copy reuses at most 512 KiB of scratch space, reduced for
|
|
131
|
+
small inputs and capped by the archive byte limit plus one overflow-probe byte.
|
|
132
|
+
Each read stays within the remaining budget plus that probe; deadline checks
|
|
133
|
+
surround reads, and short writes finish before the buffer is reused.
|
|
134
|
+
|
|
129
135
|
Native extraction is deliberately split into two phases. Rust first reports an
|
|
130
136
|
entry manifest without creating paths. TypeScript validates paths, applies
|
|
131
137
|
`stripComponents`, filters, limits, and mode policy, then passes an explicit
|
|
@@ -133,7 +139,13 @@ accepted-entry plan back to Rust. Rust owns raw-stream admission, decompression,
|
|
|
133
139
|
and fd-relative `mkdirBeneath`/exclusive-open writes. This keeps filter policy identical
|
|
134
140
|
between native and JavaScript paths rather than reimplementing it in Rust.
|
|
135
141
|
|
|
136
|
-
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected.
|
|
142
|
+
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
143
|
+
|
|
144
|
+
Within one ZIP entry, identical local and central name bytes reuse the same
|
|
145
|
+
decoded validation. Unicode Path admission is shared only when both the raw names
|
|
146
|
+
and the complete Unicode fields match; different fields still verify their own
|
|
147
|
+
CRC and interpretation. Shared backing memory is checked independently. Decoded
|
|
148
|
+
name validation is not reused across entries or archives.
|
|
137
149
|
|
|
138
150
|
`stripComponents` removes leading nonempty, non-`.` path components after
|
|
139
151
|
normalizing separators. For example, `./pkg/hello.txt` with
|
|
@@ -222,6 +234,13 @@ record and are then cleared; local PAX on unsupported types and GNU sparse
|
|
|
222
234
|
|
|
223
235
|
If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
|
|
224
236
|
|
|
237
|
+
Extraction captures the destination's lossless filesystem identity before any
|
|
238
|
+
entry filter runs and retains that capability through final publication. If a
|
|
239
|
+
filter or concurrent actor renames or replaces the destination, extraction
|
|
240
|
+
rejects with `destination-symlink-traversal` before publishing into the
|
|
241
|
+
replacement. This check uses bigint device and inode identities so large native
|
|
242
|
+
identifiers cannot compare equal after JavaScript number rounding.
|
|
243
|
+
|
|
225
244
|
The destination merge is nontransactional: each file is published atomically,
|
|
226
245
|
but completed files and directories can remain when a later copy, post-copy
|
|
227
246
|
check, mode application, or deadline fails. This also applies to
|
|
@@ -354,7 +373,9 @@ bypass validation. Decompression remains streaming; no complete decoded archive
|
|
|
354
373
|
is retained in memory or written to a decoded spool.
|
|
355
374
|
|
|
356
375
|
The WASM transport has a fixed 64 KiB input buffer, one pending member event,
|
|
357
|
-
and a 256 MiB maximum linear memory per isolated parser instance.
|
|
376
|
+
and a 256 MiB maximum linear memory per isolated parser instance. JavaScript
|
|
377
|
+
gzip decoding emits chunks of at most 64 KiB for both staged files and buffered
|
|
378
|
+
inputs, matching that input window. Metadata is
|
|
358
379
|
bounded before allocation; allocation failure rejects. Stream backpressure
|
|
359
380
|
bounds queued chunks, and completion/error destroys the instance's parser
|
|
360
381
|
state. The manifest retains the existing charged budget below; linear memory
|
|
@@ -574,7 +595,9 @@ inputs retain the archive subpath's 256 MiB compressed-input ceiling.
|
|
|
574
595
|
With a native binding it uses the same Rust decoders as extraction, including
|
|
575
596
|
zstd and bzip2 TAR. Without native it retains the JS ZIP/TAR/gzip implementation.
|
|
576
597
|
Archive member reads retain their private in-memory input without a disk
|
|
577
|
-
snapshot.
|
|
598
|
+
snapshot. JavaScript ZIP member reads reuse their completed physical admission
|
|
599
|
+
when loading the decoder, which still checks its decoded names and entry count.
|
|
600
|
+
The native ZIP reader retains the private allocation and parsed directory across worker-thread
|
|
578
601
|
inspection and reading without an extra archive-byte copy.
|
|
579
602
|
Decompression still allocates its bounded output; Node receives that native
|
|
580
603
|
allocation without another copy where external buffers are supported.
|
|
@@ -582,8 +605,11 @@ Native TAR retains the fully admitted member offsets alongside the same input
|
|
|
582
605
|
allocation. Plain TAR copies only the selected payload range after full archive
|
|
583
606
|
validation. Gzip, zstd, and bzip2 replay bounded decompression and still validate
|
|
584
607
|
all framing, trailers, and physical padding before returning. The JavaScript
|
|
585
|
-
TAR/gzip fallback
|
|
586
|
-
|
|
608
|
+
TAR/gzip fallback copies each input window into WASM once, consuming member
|
|
609
|
+
events at offsets within that window. After full admission, plain TAR copies the
|
|
610
|
+
selected range directly from its private snapshot; gzip still replays bounded
|
|
611
|
+
decompression through the parser. WASM transport and selected output still
|
|
612
|
+
require copies.
|
|
587
613
|
Returned buffers own their bytes, so changing a result cannot modify an archive
|
|
588
614
|
reader or retain an unrelated part of the input through its backing ArrayBuffer.
|
|
589
615
|
|
package/docs/atomic.md
CHANGED
|
@@ -40,7 +40,7 @@ type ReplaceFileAtomicOptions = {
|
|
|
40
40
|
content: string | Uint8Array;
|
|
41
41
|
dirMode?: number; // parent-directory mode (POSIX; default 0o700)
|
|
42
42
|
mode?: number; // new-file mode (default 0o600)
|
|
43
|
-
preserveExistingMode?: boolean; //
|
|
43
|
+
preserveExistingMode?: boolean; // inherit existing regular-file rwx bits; default false
|
|
44
44
|
tempPrefix?: string; // default ".fs-safe-replace"
|
|
45
45
|
renameMaxRetries?: number; // EBUSY retries; default 0
|
|
46
46
|
renameRetryBaseDelayMs?: number; // exponential base; default 50
|
|
@@ -57,6 +57,17 @@ type ReplaceFileAtomicOptions = {
|
|
|
57
57
|
};
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
`preserveExistingMode` snapshots only the ordinary rwx bits (`0o777`) from an
|
|
61
|
+
existing non-symlink regular destination. A final symlink fails with
|
|
62
|
+
`FsSafeError("symlink")`; a directory or other non-regular destination fails
|
|
63
|
+
with `FsSafeError("not-file")`. Set-user-ID, set-group-ID, and sticky bits are
|
|
64
|
+
never inherited. Mode inheritance does not copy ownership, ACLs, extended
|
|
65
|
+
attributes, or exact destination identity. Rename publication creates a new
|
|
66
|
+
inode; an in-place copy fallback can retain metadata already attached to its
|
|
67
|
+
pinned destination. The snapshot does not make replacement a compare-and-swap
|
|
68
|
+
operation, so the destination parent must still be protected from untrusted
|
|
69
|
+
concurrent namespace mutation.
|
|
70
|
+
|
|
60
71
|
### `beforeRename`
|
|
61
72
|
|
|
62
73
|
Runs after the temp file is fully written and before the rename. Use it to take a backup snapshot, capture the about-to-be-replaced contents, or notify an observer. The helper retains the staged descriptor and exact bigint identity across the hook; replacing, deleting, hardlinking, or changing the temp entry to a non-regular file is rejected before publication:
|
|
@@ -135,6 +146,11 @@ than `maxRestoreBytes` fails with `too-large` before mutation. A missing
|
|
|
135
146
|
destination has no original to restore and follows the exclusive-create copy
|
|
136
147
|
fallback.
|
|
137
148
|
|
|
149
|
+
Restore snapshots use the pinned file's size as an allocation hint, with an
|
|
150
|
+
initial allocation capped at 16 MiB plus the overflow byte. Reads continue
|
|
151
|
+
through short reads and EOF, grow only as data arrives, and enforce the same
|
|
152
|
+
`maxRestoreBytes` budget even if the destination grows after its size was read.
|
|
153
|
+
|
|
138
154
|
### Sync variant
|
|
139
155
|
|
|
140
156
|
`replaceFileAtomicSync` accepts the same base options, a synchronous
|
package/docs/config.md
CHANGED
|
@@ -65,6 +65,7 @@ type FsSafeLockConfig = {
|
|
|
65
65
|
Set process-wide defaults for sidecar lock options. This does **not** turn locking on globally; callers still need to pass `lock: true` or a lock options object for the specific JSON store/resource that needs cross-process coordination.
|
|
66
66
|
|
|
67
67
|
`staleRecovery` defaults to `"fail-closed"`. The opt-in `"remove-if-unchanged"` mode requires caller approval and serializes the final snapshot check and unlink with an exclusive `.reclaim` guard. A reclaim guard left by a killed reclaimer fails closed and requires externally coordinated cleanup.
|
|
68
|
+
`staleMs` must be non-negative and not `NaN`; `Infinity` disables age-based staleness.
|
|
68
69
|
|
|
69
70
|
For a daemon that should wait briefly for normal contention but never delete a
|
|
70
71
|
stale owner without per-lock approval:
|
package/docs/contributing.md
CHANGED
|
@@ -42,6 +42,32 @@ Vitest. Tests live in `test/` and follow `*.test.ts`. Run a single file with:
|
|
|
42
42
|
pnpm test test/archive.test.ts
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
Guest filesystem tests and package smoke also need `python3` on Linux and
|
|
46
|
+
macOS. They execute the exported source on synthetic files; Windows checks
|
|
47
|
+
the import surface and leaves POSIX execution to the Linux/macOS lanes.
|
|
48
|
+
After building on Linux, `node scripts/check-pack.mjs --guest-cross-device`
|
|
49
|
+
also proves an installed-package directory move from temporary storage to
|
|
50
|
+
`/dev/shm`; the command requires those locations to be different filesystems.
|
|
51
|
+
|
|
52
|
+
With Bun 1.4.2 installed, build the host addon and run the native compatibility
|
|
53
|
+
lane in real Bun workers, then exercise the built package with JIT disabled:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm native:build
|
|
57
|
+
pnpm test:bun:native
|
|
58
|
+
bun --jitless scripts/bun-native-proof.mjs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Keep the Node/pnpm build toolchain above. CI runs the native compatibility lane
|
|
62
|
+
on Linux, macOS, and Windows. The built-package proof checks `auto`/`require`,
|
|
63
|
+
native-off loading policy, and a separate installation without the addon.
|
|
64
|
+
|
|
65
|
+
`pnpm test:bun` runs the entire Node-oriented suite as a diagnostic. On released
|
|
66
|
+
Bun, its explicit native-off and missing-helper cases include unsupported
|
|
67
|
+
permission/path behavior described in [install](install.md#bun-runtime); this
|
|
68
|
+
command is not a passing compatibility gate. Node CI retains every fallback
|
|
69
|
+
assertion. Neither lane rewrites `off` to `auto` or marks defects as expected passes.
|
|
70
|
+
|
|
45
71
|
Use `vi.mock` sparingly. Most tests should drive real disk operations in a `mkdtemp`-created scratch directory, asserting on observable behavior. The library has [test hooks](testing.md) for the rare cases where you need to inject a TOCTOU race deterministically.
|
|
46
72
|
|
|
47
73
|
Vitest timeouts do not cancel filesystem promises. Shared fixtures with expensive
|
|
@@ -50,7 +76,10 @@ setup use `useSuiteFixture` from `test/helpers/suite-fixture.ts`: setup has a se
|
|
|
50
76
|
removing directories. Run shared-state corpora sequentially with a deadline per
|
|
51
77
|
payload. Keep child-process liveness limits separate from fixture preparation.
|
|
52
78
|
The Windows CI slow-copy proof runs the real package-copy process-exit test with a
|
|
53
|
-
|
|
79
|
+
16-second copy delay, a 10-second child deadline, a 15-second test deadline, and
|
|
80
|
+
60-second setup and teardown hook budgets. Other hosts retain the ordinary
|
|
81
|
+
six-second delay, four-second child deadline, five-second test deadline, and
|
|
82
|
+
30-second hook budgets:
|
|
54
83
|
|
|
55
84
|
```bash
|
|
56
85
|
pnpm build
|
|
@@ -123,7 +152,9 @@ collection uses the actual seven collected native tarballs instead. Run it with
|
|
|
123
152
|
`pnpm package:collect` after assembling all seven real bindings; missing targets
|
|
124
153
|
fail collection. `pnpm package:collect --allow-host-only` exercises the same
|
|
125
154
|
lifecycle boundary locally but proves only the host. Both collection commands
|
|
126
|
-
require the pnpm lifecycle CLI path;
|
|
155
|
+
require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standalone
|
|
156
|
+
`@pnpm/exe` binaries run directly. Shell/cmd shims and PATH fallback are not used;
|
|
157
|
+
direct `node` invocation without lifecycle metadata is unsupported. Archive
|
|
127
158
|
codecs and their dependencies are packed from the installed dependency graph.
|
|
128
159
|
|
|
129
160
|
PR CI builds and executes four host targets: Linux x64 glibc, Linux x64 musl
|