@openclaw/fs-safe 0.9.0 → 0.11.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 +134 -0
- package/LICENSE +1 -0
- package/README.md +52 -4
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- 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 +4 -5
- package/dist/archive-gzip-tail.d.ts +3 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +23 -3
- 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 +16 -13
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -6
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +83 -74
- 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 +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +23 -10
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +18 -14
- 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 +13 -8
- 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 +12 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +82 -45
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +22 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +69 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +222 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +125 -0
- 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/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -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 +9 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +135 -39
- 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 +6 -4
- 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.map +1 -1
- package/dist/filename.js +4 -13
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +4 -2
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- 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 +413 -0
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +5 -8
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +19 -18
- 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 +24 -25
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +56 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -34
- package/dist/mutation-authority.d.ts +9 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +29 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +19 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +3 -1
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- 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.d.ts.map +1 -1
- package/dist/path.js +5 -2
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +85 -53
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +7 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +51 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- 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-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- 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-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +53 -28
- 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 +26 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +219 -0
- 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 +6 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +16 -12
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +17 -51
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +358 -234
- package/dist/root-options.d.ts +77 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +59 -3
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +13 -9
- 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 +14 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +3 -5
- package/dist/root.d.ts +4 -1
- package/dist/root.d.ts.map +1 -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.d.ts.map +1 -1
- package/dist/secure-file.js +25 -8
- 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 +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +42 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +6 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +17 -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 +20 -1
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +12 -6
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +9 -7
- 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 +9 -28
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +26 -0
- package/docs/advanced.md +22 -4
- package/docs/archive.md +44 -9
- package/docs/atomic.md +24 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +36 -1
- package/docs/copy.md +155 -0
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +53 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +4 -4
- package/docs/file-store.md +15 -0
- package/docs/guest.md +141 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install.md +31 -0
- package/docs/local-roots.md +8 -1
- package/docs/native-helper.md +10 -3
- package/docs/native.md +18 -1
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/permissions.md +29 -10
- package/docs/positional-read.md +63 -0
- package/docs/public-api.md +31 -2
- package/docs/root.md +196 -6
- package/docs/secure-file.md +2 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +12 -3
- package/docs/temp.md +35 -6
- package/docs/timing.md +2 -0
- package/docs/types.md +19 -12
- package/docs/walk.md +54 -5
- package/docs/writing.md +173 -5
- package/package.json +19 -8
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fsSync, {} from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, writeNativeInput, } from "./native-operations.js";
|
|
6
7
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
8
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
7
9
|
export function sameNativeIdentity(left, right) {
|
|
8
10
|
return left.dev === right.dev && left.ino === right.ino;
|
|
9
11
|
}
|
|
@@ -27,6 +29,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
27
29
|
let completed = false;
|
|
28
30
|
try {
|
|
29
31
|
tempName = `.fs-safe-${randomUUID()}.tmp`;
|
|
32
|
+
params.assertBeforeMutation?.();
|
|
30
33
|
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
|
|
31
34
|
const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
|
|
32
35
|
tempIdentity = verificationIdentity;
|
|
@@ -34,9 +37,13 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
34
37
|
// can remove owner access. Keep the unpublished inode private and
|
|
35
38
|
// reopenable until the published name has been identity-fenced.
|
|
36
39
|
fsSync.fchmodSync(tempFd, 0o600);
|
|
37
|
-
await writeNativeInput(tempFd, params.input, params.maxBytes);
|
|
40
|
+
await writeNativeInput(tempFd, params.input, params.maxBytes, params.assertBeforeMutation);
|
|
38
41
|
if (params.sync !== false)
|
|
39
42
|
syncFileBestEffortSync(tempFd);
|
|
43
|
+
if (params.input.kind === "file")
|
|
44
|
+
await params.input.verifySource();
|
|
45
|
+
assertFinalSymlinkRejected(path.join(parentPath, params.basename), params.rejectFinalSymlink);
|
|
46
|
+
params.assertBeforeMutation?.();
|
|
40
47
|
if (params.overwrite === false) {
|
|
41
48
|
binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
|
|
42
49
|
}
|
|
@@ -44,6 +51,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
44
51
|
binding.renameReplace(parentFd, tempName, parentFd, params.basename);
|
|
45
52
|
}
|
|
46
53
|
renamed = true;
|
|
54
|
+
params.onPublished?.(verificationIdentity);
|
|
47
55
|
targetFd = binding.openBeneath(parentFd, params.basename, nativeOpenFlags(fsSync.constants.O_RDONLY)).fd;
|
|
48
56
|
const targetStat = inspectFileIdentitySync(() => fsSync.fstatSync(targetFd, { bigint: true }), verificationIdentity);
|
|
49
57
|
const targetIdentity = binding.fstatIdentity(targetFd);
|
|
@@ -61,12 +69,16 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
61
69
|
catch (error) {
|
|
62
70
|
closeWriteFd(targetFd);
|
|
63
71
|
targetFd = undefined;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
// Staged copies and streamed creates preserve publication for caller recovery.
|
|
73
|
+
if (params.input.kind === "buffer" ||
|
|
74
|
+
(params.input.kind === "stream" && !params.input.stageBeforePublish)) {
|
|
75
|
+
removeNativeCreatedFileIfStillPinned({
|
|
76
|
+
parentPath,
|
|
77
|
+
parentFd,
|
|
78
|
+
basename: params.basename,
|
|
79
|
+
created: tempIdentity,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
70
82
|
throw error;
|
|
71
83
|
}
|
|
72
84
|
if (params.sync !== false)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2FvH"}
|
|
@@ -59,6 +59,7 @@ import { runPinnedWriteWindows, sameNativeIdentity } from "./native-pinned-write
|
|
|
59
59
|
import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
|
|
60
60
|
import { describeStagedDirectory, exactIdentityMatches } from "./staged-directory.js";
|
|
61
61
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
62
|
+
import { realpathSync } from "./realpath.js";
|
|
62
63
|
export async function runPinnedWriteNative(binding, params) {
|
|
63
64
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
64
65
|
try {
|
|
@@ -100,10 +101,11 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
100
101
|
throw new FsSafeError("path-mismatch", "root path changed during native write");
|
|
101
102
|
}
|
|
102
103
|
if (params.mkdir) {
|
|
104
|
+
params.assertBeforeMutation?.();
|
|
103
105
|
binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
|
|
104
106
|
}
|
|
105
107
|
parentFd = binding.openBeneath(root.fd, params.relativeParentPath, directoryFlags).fd;
|
|
106
|
-
const parentPath =
|
|
108
|
+
const parentPath = realpathSync.native(params.relativeParentPath
|
|
107
109
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
108
110
|
: params.rootPath);
|
|
109
111
|
const directory = windows ? undefined : describeStagedDirectory(parentFd, parentPath);
|
|
@@ -8,14 +8,14 @@ export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding,
|
|
|
8
8
|
export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
|
|
9
9
|
declare class NativeStagedFile implements StagedFile {
|
|
10
10
|
#private;
|
|
11
|
-
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean);
|
|
12
|
-
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean): Promise<NativeStagedFile>;
|
|
11
|
+
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
|
|
12
|
+
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
|
|
13
13
|
static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
14
14
|
get receipt(): StagedFileReceipt;
|
|
15
15
|
assertCurrent(): Promise<void>;
|
|
16
16
|
publish(basename: string, options: {
|
|
17
17
|
overwrite: boolean;
|
|
18
|
-
}): Promise<PublishedFileReceipt>;
|
|
18
|
+
}, onPublished?: PinnedWriteParams["onPublished"]): Promise<PublishedFileReceipt>;
|
|
19
19
|
cleanup(): Promise<StagedFileCleanupReceipt>;
|
|
20
20
|
[Symbol.asyncDispose](): Promise<void>;
|
|
21
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAa1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,OAAO,EACb,oBAAoB,CAAC,EAAE,MAAM,IAAI,EASlC;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,EACpB,IAAI,UAAO,EACX,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IA8FK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,EAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAC7C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC;AAEvD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
|
|
@@ -54,9 +54,13 @@ import { requireNativeBinding } from "./native.js";
|
|
|
54
54
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
55
55
|
import { randomUUID } from "node:crypto";
|
|
56
56
|
import fs from "node:fs";
|
|
57
|
+
import path from "node:path";
|
|
57
58
|
import { FsSafeError } from "./errors.js";
|
|
59
|
+
import { MutationAuthorityError } from "./mutation-authority.js";
|
|
58
60
|
import { writeNativeInput } from "./native-operations.js";
|
|
61
|
+
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
59
62
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
63
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
60
64
|
export function assertNativeStaging(binding) {
|
|
61
65
|
if ([
|
|
62
66
|
binding.createStagedFile,
|
|
@@ -93,24 +97,27 @@ class NativeStagedFile {
|
|
|
93
97
|
#portableNames;
|
|
94
98
|
#publishedMode;
|
|
95
99
|
#sync;
|
|
100
|
+
#assertBeforeMutation;
|
|
96
101
|
#name = `.fs-safe-${randomUUID()}.tmp`;
|
|
97
102
|
#state = { status: "open", publication: NOT_PUBLISHED };
|
|
98
103
|
#receipt;
|
|
99
|
-
|
|
104
|
+
#rejectFinalSymlink = false;
|
|
105
|
+
constructor(binding, parentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
|
|
100
106
|
this.#binding = binding;
|
|
101
107
|
this.#parentFd = parentFd;
|
|
102
108
|
this.#directory = directory;
|
|
103
109
|
this.#portableNames = portableNames;
|
|
104
110
|
this.#publishedMode = publishedMode;
|
|
105
111
|
this.#sync = sync;
|
|
112
|
+
this.#assertBeforeMutation = assertBeforeMutation;
|
|
106
113
|
}
|
|
107
114
|
// Takes ownership of parentFd, including all construction and preparation failures.
|
|
108
115
|
static async create(binding, parentFd, directory, input, mode, maxBytes,
|
|
109
116
|
// Public staging uses portable names; existing POSIX writes accept literal names.
|
|
110
|
-
portableNames = true, sync = true) {
|
|
117
|
+
portableNames = true, sync = true, assertBeforeMutation) {
|
|
111
118
|
let staged;
|
|
112
119
|
try {
|
|
113
|
-
staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync);
|
|
120
|
+
staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync, assertBeforeMutation);
|
|
114
121
|
}
|
|
115
122
|
catch (error) {
|
|
116
123
|
try {
|
|
@@ -129,8 +136,11 @@ class NativeStagedFile {
|
|
|
129
136
|
try {
|
|
130
137
|
// This owner never escapes. Only the internal verifier borrows its fd;
|
|
131
138
|
// public descriptor methods remain await-free and cannot race disposal.
|
|
132
|
-
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync), true);
|
|
133
|
-
|
|
139
|
+
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
|
|
140
|
+
staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
|
|
141
|
+
if (params.input.kind === "file")
|
|
142
|
+
await params.input.verifySource();
|
|
143
|
+
const published = await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
|
|
134
144
|
const identity = published.staged.identity;
|
|
135
145
|
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
136
146
|
return { dev: identity.dev, ino: identity.ino };
|
|
@@ -190,10 +200,22 @@ class NativeStagedFile {
|
|
|
190
200
|
// The exclusive open performs no fallible post-open checks. Store its fd
|
|
191
201
|
// before every subsequent operation, including the first metadata read.
|
|
192
202
|
const state = this.#open();
|
|
193
|
-
|
|
203
|
+
this.#assertBeforeMutation?.();
|
|
204
|
+
const copied = input.kind === "file"
|
|
205
|
+
? await createNativeCopyFile(this.#binding, input, this.#parentFd, this.#name, maxBytes, false)
|
|
206
|
+
: undefined;
|
|
207
|
+
state.fileFd = copied?.fd;
|
|
208
|
+
if (state.fileFd === undefined) {
|
|
209
|
+
this.#assertBeforeMutation?.();
|
|
210
|
+
state.fileFd = this.#binding.createStagedFile(this.#parentFd, this.#name);
|
|
211
|
+
}
|
|
194
212
|
const fd = state.fileFd;
|
|
213
|
+
if (input.kind === "file")
|
|
214
|
+
assertNativeCopyCompleted(input, copied);
|
|
215
|
+
this.#assertBeforeMutation?.();
|
|
195
216
|
fs.fchmodSync(fd, 0o600);
|
|
196
|
-
|
|
217
|
+
if (!copied)
|
|
218
|
+
await writeNativeInput(fd, input, maxBytes, this.#assertBeforeMutation);
|
|
197
219
|
if (this.#sync)
|
|
198
220
|
syncFileBestEffortSync(fd);
|
|
199
221
|
const stat = fs.fstatSync(fd, { bigint: true });
|
|
@@ -230,7 +252,7 @@ class NativeStagedFile {
|
|
|
230
252
|
async assertCurrent() {
|
|
231
253
|
this.#assertCurrent();
|
|
232
254
|
}
|
|
233
|
-
async publish(basename, options) {
|
|
255
|
+
async publish(basename, options, onPublished) {
|
|
234
256
|
const overwrite = options?.overwrite;
|
|
235
257
|
try {
|
|
236
258
|
const state = this.#open();
|
|
@@ -239,6 +261,8 @@ class NativeStagedFile {
|
|
|
239
261
|
throw new FsSafeError("invalid-path", "publication needs a distinct basename and explicit overwrite policy");
|
|
240
262
|
}
|
|
241
263
|
this.#assertCurrent();
|
|
264
|
+
assertFinalSymlinkRejected(path.join(this.#directory.realPath, basename), this.#rejectFinalSymlink);
|
|
265
|
+
this.#assertBeforeMutation?.();
|
|
242
266
|
try {
|
|
243
267
|
if (overwrite) {
|
|
244
268
|
this.#binding.renameReplace(this.#parentFd, this.#name, this.#parentFd, basename);
|
|
@@ -262,6 +286,7 @@ class NativeStagedFile {
|
|
|
262
286
|
overwrite,
|
|
263
287
|
});
|
|
264
288
|
state.publication = receipt;
|
|
289
|
+
onPublished?.(receipt.staged.identity);
|
|
265
290
|
const stagedMode = this.#assertNamed(basename);
|
|
266
291
|
assertStagedDirectoryCurrent(this.#directory);
|
|
267
292
|
// Keep contents private until the published name passes its identity
|
|
@@ -282,6 +307,8 @@ class NativeStagedFile {
|
|
|
282
307
|
return receipt;
|
|
283
308
|
}
|
|
284
309
|
catch (error) {
|
|
310
|
+
if (error instanceof MutationAuthorityError)
|
|
311
|
+
throw error;
|
|
285
312
|
// Closure rejects further use, not the recorded outcome of an earlier publication.
|
|
286
313
|
const publication = this.#state.status === "closed"
|
|
287
314
|
? this.#state.receipt.publication
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opened-realpath.d.ts","sourceRoot":"","sources":["../src/opened-realpath.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"opened-realpath.d.ts","sourceRoot":"","sources":["../src/opened-realpath.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAI7E,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,MAAM,EACV,UAAU,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACxC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AACpD,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,GAAG,WAAW,CAAA;CAAE,CAAC,CAAC"}
|
package/dist/opened-realpath.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { openedPathResolutionError } from "./opened-file-failure.js";
|
|
5
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { isNotFoundPathError } from "./path.js";
|
|
7
|
+
import { realpathSync } from "./realpath.js";
|
|
7
8
|
export async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
|
|
8
9
|
const handleStat = fsSync.fstatSync(handle.fd);
|
|
9
10
|
return (await resolveOpenedFileRealPathForFd(handle.fd, handleStat, ioPath)).realPath;
|
|
@@ -16,7 +17,7 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
16
17
|
: [];
|
|
17
18
|
for (const fdPath of fdCandidates) {
|
|
18
19
|
try {
|
|
19
|
-
const fdRealPath =
|
|
20
|
+
const fdRealPath = realpathSync.native(fdPath);
|
|
20
21
|
const fdRealStat = statOptions ? fsSync.statSync(fdRealPath, statOptions) : fsSync.statSync(fdRealPath);
|
|
21
22
|
if (sameFileIdentity(handleStat, fdRealStat)) {
|
|
22
23
|
return { realPath: fdRealPath, stat: fdRealStat };
|
|
@@ -27,7 +28,7 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
try {
|
|
30
|
-
const ioRealPath =
|
|
31
|
+
const ioRealPath = realpathSync.native(ioPath);
|
|
31
32
|
const ioRealStat = statOptions ? fsSync.statSync(ioRealPath, statOptions) : fsSync.statSync(ioRealPath);
|
|
32
33
|
if (sameFileIdentity(handleStat, ioRealStat)) {
|
|
33
34
|
return { realPath: ioRealPath, stat: ioRealStat };
|
|
@@ -53,7 +54,7 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
53
54
|
async function resolveOpenedFileRealPathFromParent(handleStat, ioPath, statOptions) {
|
|
54
55
|
let parentReal;
|
|
55
56
|
try {
|
|
56
|
-
parentReal =
|
|
57
|
+
parentReal = realpathSync.native(path.dirname(ioPath));
|
|
57
58
|
}
|
|
58
59
|
catch (err) {
|
|
59
60
|
if (isNotFoundPathError(err)) {
|
|
@@ -76,7 +77,7 @@ async function resolveOpenedFileRealPathFromParent(handleStat, ioPath, statOptio
|
|
|
76
77
|
try {
|
|
77
78
|
const candidateStat = statOptions ? fsSync.lstatSync(candidatePath, statOptions) : fsSync.lstatSync(candidatePath);
|
|
78
79
|
if (candidateStat.isFile() && sameFileIdentity(handleStat, candidateStat)) {
|
|
79
|
-
const realPath =
|
|
80
|
+
const realPath = realpathSync.native(candidatePath);
|
|
80
81
|
const stat = statOptions ? fsSync.statSync(realPath, statOptions) : fsSync.statSync(realPath);
|
|
81
82
|
if (sameFileIdentity(handleStat, stat))
|
|
82
83
|
return { realPath, stat };
|
package/dist/output.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type ExternalFileWriteOptions<T = void> = {
|
|
|
5
5
|
maxBytes?: number;
|
|
6
6
|
mode?: number;
|
|
7
7
|
staging?: "workspace" | "sibling";
|
|
8
|
+
/** Isolate sibling producers; workspace staging is already private. */
|
|
9
|
+
producerIsolation?: "private-directory";
|
|
8
10
|
fallbackFileName?: string;
|
|
9
11
|
};
|
|
10
12
|
export type ExternalFileWriteResult<T = void> = {
|
package/dist/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAiDrC"}
|
package/dist/output.js
CHANGED
|
@@ -74,6 +74,7 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
74
74
|
const result = await writeExternalFileViaSibling({
|
|
75
75
|
finalPath: siblingFinalPath,
|
|
76
76
|
write: options.write,
|
|
77
|
+
producerIsolation: options.producerIsolation,
|
|
77
78
|
fallbackFileName: options.fallbackFileName,
|
|
78
79
|
maxBytes,
|
|
79
80
|
mode: options.mode,
|
|
@@ -113,6 +114,7 @@ async function writeExternalFileViaSibling(params) {
|
|
|
113
114
|
const { result } = await writeCallbackSibling({
|
|
114
115
|
tempPath: buildSiblingTempPath(finalPath, params.fallbackFileName),
|
|
115
116
|
write: params.write,
|
|
117
|
+
producerIsolation: params.producerIsolation,
|
|
116
118
|
resolveFinalPath: () => finalPath,
|
|
117
119
|
mode: params.mode,
|
|
118
120
|
maxBytes: params.maxBytes,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
export type OverwriteFileHandleOptions = {
|
|
3
|
+
/** Admit the whole write once, after rollback preparation and before any mutation. */
|
|
4
|
+
beforeWrite?: () => void;
|
|
5
|
+
};
|
|
6
|
+
/** Overwrite a borrowed, non-append read/write handle with best-effort failure rollback. */
|
|
7
|
+
export declare function overwriteFileHandle(handle: FileHandle, data: Uint8Array, options?: OverwriteFileHandleOptions): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=overwrite-file-handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overwrite-file-handle.d.ts","sourceRoot":"","sources":["../src/overwrite-file-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,MAAM,MAAM,0BAA0B,GAAG;IACvC,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAEF,4FAA4F;AAC5F,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAoCf"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
3
|
+
import { readFileWindowFully } from "./positional-read.js";
|
|
4
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
5
|
+
/** Overwrite a borrowed, non-append read/write handle with best-effort failure rollback. */
|
|
6
|
+
export async function overwriteFileHandle(handle, data, options = {}) {
|
|
7
|
+
const payload = data.subarray(0, data.byteLength);
|
|
8
|
+
const payloadBytes = payload.byteLength;
|
|
9
|
+
const stat = await handle.stat();
|
|
10
|
+
if (!stat.isFile()) {
|
|
11
|
+
throw new FsSafeError("not-file", "overwrite handle must be a regular file");
|
|
12
|
+
}
|
|
13
|
+
const originalSize = stat.size;
|
|
14
|
+
if (!Number.isSafeInteger(originalSize) || originalSize < 0) {
|
|
15
|
+
throw new RangeError("file size must be a non-negative safe integer");
|
|
16
|
+
}
|
|
17
|
+
const prefixBytes = Math.min(payloadBytes, originalSize);
|
|
18
|
+
const originalPrefix = Buffer.allocUnsafe(prefixBytes);
|
|
19
|
+
if (await readFileWindowFully(handle, originalPrefix, 0) !== prefixBytes) {
|
|
20
|
+
throw new FsSafeError("read-failed", "file ended before overwrite rollback preparation completed");
|
|
21
|
+
}
|
|
22
|
+
assertSynchronousCallbackResult(options.beforeWrite?.(), "beforeWrite");
|
|
23
|
+
let prefixStarted = false;
|
|
24
|
+
try {
|
|
25
|
+
// Leave existing bytes intact until any additional storage has been written.
|
|
26
|
+
if (payloadBytes > originalSize) {
|
|
27
|
+
await writeAllToFile(handle, payload.subarray(originalSize), { position: originalSize });
|
|
28
|
+
}
|
|
29
|
+
prefixStarted = true;
|
|
30
|
+
await writeAllToFile(handle, payload.subarray(0, prefixBytes), { position: 0 });
|
|
31
|
+
if (payloadBytes < originalSize) {
|
|
32
|
+
await handle.truncate(payloadBytes);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (prefixStarted) {
|
|
37
|
+
await writeAllToFile(handle, originalPrefix, { position: 0 }).catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
await handle.truncate(originalSize).catch(() => undefined);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ProbePathCaseOptions = {
|
|
2
|
+
/** Allow an exclusively created empty probe when existing entries provide no answer. Defaults to true. */
|
|
3
|
+
allowTemporaryProbe?: boolean;
|
|
4
|
+
};
|
|
5
|
+
/** Observe local ASCII case behavior; unknown observations never imply an operating-system default. */
|
|
6
|
+
export declare function probePathCaseInsensitiveSync(targetPath: string, options?: ProbePathCaseOptions): boolean | undefined;
|
|
7
|
+
//# sourceMappingURL=path-case.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-case.d.ts","sourceRoot":"","sources":["../src/path-case.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,oBAAoB,GAAG;IACjC,0GAA0G;IAC1G,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAmFF,uGAAuG;AACvG,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,GACjC,OAAO,GAAG,SAAS,CAqCrB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs, {} from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertDirectoryIdentitySync, inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
5
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
|
+
import { isNotFoundPathError } from "./path.js";
|
|
7
|
+
import { realpathSync } from "./realpath.js";
|
|
8
|
+
import { writeTempFileSync } from "./replace-file-descriptor.js";
|
|
9
|
+
import { SyncAtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
10
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
11
|
+
function swapAsciiCase(value) {
|
|
12
|
+
return value.replace(/[A-Za-z]/g, character => {
|
|
13
|
+
const lower = character.toLowerCase();
|
|
14
|
+
return character === lower ? character.toUpperCase() : lower;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function probeEntry(directory, name, names) {
|
|
18
|
+
const alternate = swapAsciiCase(name);
|
|
19
|
+
if (alternate === name)
|
|
20
|
+
return undefined;
|
|
21
|
+
const bothListed = names.has(name) && names.has(alternate);
|
|
22
|
+
const originalPath = path.join(directory, name);
|
|
23
|
+
let original;
|
|
24
|
+
let observed;
|
|
25
|
+
try {
|
|
26
|
+
original = inspectFileIdentitySync(() => fs.lstatSync(originalPath, { bigint: true }));
|
|
27
|
+
try {
|
|
28
|
+
const other = inspectFileIdentitySync(() => fs.lstatSync(path.join(directory, alternate), { bigint: true }));
|
|
29
|
+
const sameIdentity = sameFileIdentityForCleanup(original, other);
|
|
30
|
+
if (sameIdentity || bothListed) {
|
|
31
|
+
// Matching inodes can belong to newly linked case-distinct names.
|
|
32
|
+
const currentNames = new Set(fs.readdirSync(directory));
|
|
33
|
+
const bothCurrent = currentNames.has(name) && currentNames.has(alternate);
|
|
34
|
+
if (bothListed && !bothCurrent)
|
|
35
|
+
return undefined;
|
|
36
|
+
observed = sameIdentity && !bothCurrent;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
observed = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (bothListed || !isNotFoundPathError(error))
|
|
44
|
+
return undefined;
|
|
45
|
+
observed = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
// Disappearance during lookup is not evidence that case variants are distinct.
|
|
52
|
+
inspectFileIdentitySync(() => fs.lstatSync(originalPath, { bigint: true }), original);
|
|
53
|
+
return observed;
|
|
54
|
+
}
|
|
55
|
+
function probeTemporaryEntry(directory) {
|
|
56
|
+
const name = `.fs-safe-case-probe-${randomUUID()}`;
|
|
57
|
+
const owner = new SyncAtomicTempOwner(path.join(directory, name));
|
|
58
|
+
try {
|
|
59
|
+
owner.start();
|
|
60
|
+
owner.adopt(writeTempFileSync({
|
|
61
|
+
fsModule: fs,
|
|
62
|
+
tempPath: owner.pathname,
|
|
63
|
+
content: "",
|
|
64
|
+
mode: 0o600,
|
|
65
|
+
sync: false,
|
|
66
|
+
onIdentity: owner.onIdentity,
|
|
67
|
+
}));
|
|
68
|
+
owner.assertCurrent(fs);
|
|
69
|
+
const observed = probeEntry(directory, name, new Set(fs.readdirSync(directory)));
|
|
70
|
+
owner.assertCurrent(fs);
|
|
71
|
+
return observed;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
// Failed cleanup invalidates the observation; the existing owner retains its exit retry.
|
|
75
|
+
owner.finish({ fsModule: fs, throwOnCleanupError: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function probeDirectory(directory, allowTemporaryProbe, preferredName) {
|
|
79
|
+
const canonical = realpathSync.native(directory);
|
|
80
|
+
const identity = inspectDirectoryIdentitySync(canonical);
|
|
81
|
+
const names = new Set(fs.readdirSync(canonical));
|
|
82
|
+
let observed = preferredName === undefined ? undefined : probeEntry(canonical, preferredName, names);
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
if (observed !== undefined)
|
|
85
|
+
break;
|
|
86
|
+
observed = probeEntry(canonical, name, names);
|
|
87
|
+
}
|
|
88
|
+
if (observed === undefined && allowTemporaryProbe)
|
|
89
|
+
observed = probeTemporaryEntry(canonical);
|
|
90
|
+
assertDirectoryIdentitySync(canonical, identity);
|
|
91
|
+
return realpathSync.native(directory) === canonical ? observed : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** Observe local ASCII case behavior; unknown observations never imply an operating-system default. */
|
|
94
|
+
export function probePathCaseInsensitiveSync(targetPath, options = {}) {
|
|
95
|
+
const resolved = path.resolve(targetPath);
|
|
96
|
+
const allowTemporaryProbe = options.allowTemporaryProbe !== false;
|
|
97
|
+
let targetExists = true;
|
|
98
|
+
try {
|
|
99
|
+
fs.lstatSync(resolved);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!isNotFoundPathError(error))
|
|
103
|
+
return undefined;
|
|
104
|
+
targetExists = false;
|
|
105
|
+
}
|
|
106
|
+
if (targetExists) {
|
|
107
|
+
try {
|
|
108
|
+
return probeDirectory(path.dirname(resolved), allowTemporaryProbe, path.basename(resolved));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let directory = path.dirname(resolved);
|
|
115
|
+
for (;;) {
|
|
116
|
+
let exists = false;
|
|
117
|
+
try {
|
|
118
|
+
exists = fs.statSync(directory).isDirectory();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// A missing or unreadable component can still have a readable existing ancestor.
|
|
122
|
+
}
|
|
123
|
+
if (exists) {
|
|
124
|
+
try {
|
|
125
|
+
return probeDirectory(directory, allowTemporaryProbe);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const parent = path.dirname(directory);
|
|
132
|
+
if (parent === directory)
|
|
133
|
+
return undefined;
|
|
134
|
+
directory = parent;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA8BlE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CA0BpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
|
package/dist/path.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
|
import { isDriveRelativePath } from "./safe-path-segment.js";
|
|
5
6
|
export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, } from "./device-path.js";
|
|
6
7
|
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
@@ -49,7 +50,9 @@ export function isPathInside(root, target) {
|
|
|
49
50
|
target.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
|
|
50
51
|
!target.includes("/..") &&
|
|
51
52
|
(target === root ||
|
|
52
|
-
(target.startsWith(root) &&
|
|
53
|
+
(target.startsWith(root) &&
|
|
54
|
+
(root.charCodeAt(root.length - 1) === POSIX_SEPARATOR_CHAR_CODE ||
|
|
55
|
+
target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE)))) {
|
|
53
56
|
return true;
|
|
54
57
|
}
|
|
55
58
|
const resolvedRoot = path.resolve(root);
|
|
@@ -74,7 +77,7 @@ export function safeRealpathSync(targetPath, cache) {
|
|
|
74
77
|
return cached;
|
|
75
78
|
}
|
|
76
79
|
try {
|
|
77
|
-
const resolved =
|
|
80
|
+
const resolved = realpathSync(targetPath);
|
|
78
81
|
cache?.set(targetPath, resolved);
|
|
79
82
|
cache?.set(resolved, resolved);
|
|
80
83
|
return resolved;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PermissionCommandFailure } from "./permission-exec.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type PermissionCheck, type PermissionCheckOptions, type SafeStatResult } from "./permissions.js";
|
|
3
3
|
export type PermissionExec = (command: string, args: string[]) => Promise<{
|
|
4
4
|
stdout: string;
|
|
5
5
|
stderr: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-windows.d.ts","sourceRoot":"","sources":["../src/permissions-windows.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"permissions-windows.d.ts","sourceRoot":"","sources":["../src/permissions-windows.ts"],"names":[],"mappings":"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"}
|