@openclaw/fs-safe 0.12.0 → 0.13.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 +61 -0
- package/README.md +12 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +6 -2
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -19
- package/dist/archive-plan.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +18 -13
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -10
- package/dist/archive-tar-inspect.d.ts.map +1 -1
- package/dist/archive-tar-inspect.js +4 -1
- 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 +2 -2
- package/dist/archive-zip-directory.d.ts +3 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +13 -2
- package/dist/archive-zip-loader.d.ts +3 -2
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +30 -4
- package/dist/archive-zip-manifest.d.ts +5 -0
- package/dist/archive-zip-manifest.d.ts.map +1 -0
- package/dist/archive-zip-manifest.js +22 -0
- package/dist/archive-zip-names.d.ts +6 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +35 -14
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +3 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/darwin-acl.d.ts +4 -0
- package/dist/darwin-acl.d.ts.map +1 -0
- package/dist/darwin-acl.js +24 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +8 -2
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +67 -23
- package/dist/directory-entry-path.d.ts +3 -0
- package/dist/directory-entry-path.d.ts.map +1 -0
- package/dist/directory-entry-path.js +21 -0
- package/dist/directory-guard.d.ts +17 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +134 -48
- package/dist/directory-mode-node.d.ts +12 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +102 -4
- package/dist/effective-uid.d.ts +2 -0
- package/dist/effective-uid.d.ts.map +1 -0
- package/dist/effective-uid.js +25 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +98 -27
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +36 -6
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +29 -9
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +6 -2
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -65
- package/dist/file-store-copy-source.d.ts +5 -0
- package/dist/file-store-copy-source.d.ts.map +1 -0
- package/dist/file-store-copy-source.js +31 -0
- package/dist/file-store-path.d.ts.map +1 -1
- package/dist/file-store-path.js +4 -1
- package/dist/file-store-sync-directory.d.ts +16 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -0
- package/dist/file-store-sync-directory.js +349 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +11 -28
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +65 -20
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +8 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +176 -27
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +4 -1
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +54 -19
- package/dist/json-durable-queue-ownership.d.ts +6 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +89 -42
- package/dist/json-durable-queue-paths.d.ts +11 -0
- package/dist/json-durable-queue-paths.d.ts.map +1 -0
- package/dist/json-durable-queue-paths.js +42 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -1
- package/dist/json-durable-queue-read.js +2 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +50 -58
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +5 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +54 -22
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +4 -0
- package/dist/local-file-descriptor.d.ts +17 -0
- package/dist/local-file-descriptor.d.ts.map +1 -0
- package/dist/local-file-descriptor.js +84 -0
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -7
- package/dist/move-path-cleanup.d.ts +2 -0
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +44 -18
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +31 -6
- package/dist/native-binding.d.ts +14 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-directory-observation.d.ts +17 -0
- package/dist/native-directory-observation.d.ts.map +1 -0
- package/dist/native-directory-observation.js +37 -0
- package/dist/native-parent-admission.d.ts +31 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +124 -0
- package/dist/native-pinned-write-windows.d.ts +0 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +0 -3
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +309 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +15 -5
- package/dist/overwrite-file-handle.d.ts.map +1 -1
- package/dist/overwrite-file-handle.js +5 -1
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -0
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +7 -2
- package/dist/path-prefix.d.ts +7 -0
- package/dist/path-prefix.d.ts.map +1 -0
- package/dist/path-prefix.js +82 -0
- package/dist/path-scope-lexical.d.ts.map +1 -1
- package/dist/path-scope-lexical.js +18 -8
- package/dist/path-segment-route.d.ts +7 -0
- package/dist/path-segment-route.d.ts.map +1 -0
- package/dist/path-segment-route.js +24 -0
- package/dist/path-suffix-aliases.d.ts +10 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -0
- package/dist/path-suffix-aliases.js +386 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +16 -5
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +14 -3
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +37 -8
- package/dist/pinned-mutation-admission.d.ts +25 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -0
- package/dist/pinned-mutation-admission.js +425 -0
- package/dist/pinned-mutation-observation.d.ts +34 -0
- package/dist/pinned-mutation-observation.d.ts.map +1 -0
- package/dist/pinned-mutation-observation.js +142 -0
- package/dist/pinned-mutation-shared-route.d.ts +24 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
- package/dist/pinned-mutation-shared-route.js +70 -0
- package/dist/pinned-open.d.ts +6 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +28 -10
- package/dist/pinned-write-types.d.ts +75 -0
- package/dist/pinned-write-types.d.ts.map +1 -0
- package/dist/pinned-write-types.js +1 -0
- package/dist/pinned-write.d.ts +7 -33
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +151 -17
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +2 -0
- package/dist/private-producer-handoff.d.ts +16 -0
- package/dist/private-producer-handoff.d.ts.map +1 -0
- package/dist/private-producer-handoff.js +272 -0
- package/dist/private-temp-workspace.d.ts +2 -39
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +183 -77
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +33 -29
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +56 -45
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +12 -5
- package/dist/replace-file-temp-owner.d.ts +1 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +12 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +24 -24
- package/dist/root-boundary.d.ts +4 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +6 -1
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +57 -11
- package/dist/root-directory-creation.d.ts +13 -0
- package/dist/root-directory-creation.d.ts.map +1 -0
- package/dist/root-directory-creation.js +212 -0
- package/dist/root-directory-list.d.ts +16 -4
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +180 -39
- package/dist/root-directory.d.ts +23 -0
- package/dist/root-directory.d.ts.map +1 -0
- package/dist/root-directory.js +141 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +12 -1
- package/dist/root-file-final-admission.d.ts +21 -0
- package/dist/root-file-final-admission.d.ts.map +1 -0
- package/dist/root-file-final-admission.js +83 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +103 -24
- package/dist/root-impl.d.ts +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +289 -317
- package/dist/root-move-noreplace.d.ts +14 -0
- package/dist/root-move-noreplace.d.ts.map +1 -0
- package/dist/root-move-noreplace.js +202 -0
- package/dist/root-observed-path.d.ts +9 -0
- package/dist/root-observed-path.d.ts.map +1 -0
- package/dist/root-observed-path.js +95 -0
- package/dist/root-path-errors.d.ts +12 -0
- package/dist/root-path-errors.d.ts.map +1 -0
- package/dist/root-path-errors.js +13 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +53 -20
- package/dist/root-path-observation.d.ts +63 -0
- package/dist/root-path-observation.d.ts.map +1 -0
- package/dist/root-path-observation.js +180 -0
- package/dist/root-path-stat.d.ts +5 -0
- package/dist/root-path-stat.d.ts.map +1 -0
- package/dist/root-path-stat.js +101 -0
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +23 -4
- package/dist/root-path.d.ts +11 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +215 -53
- package/dist/root-paths-lexical.d.ts +9 -0
- package/dist/root-paths-lexical.d.ts.map +1 -0
- package/dist/root-paths-lexical.js +22 -0
- package/dist/root-paths.d.ts +3 -25
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +77 -154
- package/dist/root-read-admission.d.ts +26 -0
- package/dist/root-read-admission.d.ts.map +1 -0
- package/dist/root-read-admission.js +94 -0
- package/dist/root-remove-identity.d.ts +15 -0
- package/dist/root-remove-identity.d.ts.map +1 -0
- package/dist/root-remove-identity.js +89 -0
- package/dist/root-remove-receipt.d.ts +17 -0
- package/dist/root-remove-receipt.d.ts.map +1 -0
- package/dist/root-remove-receipt.js +37 -0
- package/dist/root-remove.d.ts +2 -1
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +172 -10
- package/dist/root-write-admission.d.ts +68 -0
- package/dist/root-write-admission.d.ts.map +1 -0
- package/dist/root-write-admission.js +322 -0
- package/dist/root-write-compatibility.d.ts +13 -0
- package/dist/root-write-compatibility.d.ts.map +1 -0
- package/dist/root-write-compatibility.js +74 -0
- package/dist/root-write-complete-parent.d.ts +42 -0
- package/dist/root-write-complete-parent.d.ts.map +1 -0
- package/dist/root-write-complete-parent.js +195 -0
- package/dist/root-write-publication.d.ts +24 -0
- package/dist/root-write-publication.d.ts.map +1 -0
- package/dist/root-write-publication.js +85 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -4
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +62 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +18 -13
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +5 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +92 -7
- package/dist/secure-temp-dir.d.ts +6 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +121 -101
- package/dist/secure-temp-repair.d.ts +35 -0
- package/dist/secure-temp-repair.d.ts.map +1 -0
- package/dist/secure-temp-repair.js +106 -0
- package/dist/sibling-staged-file.d.ts +3 -2
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +179 -55
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -13
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +44 -14
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/staged-directory.d.ts +14 -5
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +54 -3
- package/dist/standalone-publication-path.d.ts +2 -0
- package/dist/standalone-publication-path.d.ts.map +1 -0
- package/dist/standalone-publication-path.js +6 -0
- package/dist/stat-observation.d.ts +11 -0
- package/dist/stat-observation.d.ts.map +1 -0
- package/dist/stat-observation.js +64 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +39 -2
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +7 -2
- package/dist/temp-target.d.ts +2 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +130 -3
- package/dist/temp-workspace-admission.d.ts +22 -0
- package/dist/temp-workspace-admission.d.ts.map +1 -0
- package/dist/temp-workspace-admission.js +374 -0
- package/dist/temp-workspace-child-admission.d.ts +13 -0
- package/dist/temp-workspace-child-admission.d.ts.map +1 -0
- package/dist/temp-workspace-child-admission.js +95 -0
- package/dist/temp-workspace-descriptor.d.ts +48 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -0
- package/dist/temp-workspace-descriptor.js +363 -0
- package/dist/temp-workspace-identity.d.ts +15 -0
- package/dist/temp-workspace-identity.d.ts.map +1 -0
- package/dist/temp-workspace-identity.js +41 -0
- package/dist/temp-workspace-owner.d.ts +7 -6
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +121 -61
- package/dist/temp-workspace-permissions.d.ts +4 -0
- package/dist/temp-workspace-permissions.d.ts.map +1 -0
- package/dist/temp-workspace-permissions.js +32 -0
- package/dist/temp-workspace-types.d.ts +40 -0
- package/dist/temp-workspace-types.d.ts.map +1 -0
- package/dist/temp-workspace-types.js +1 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/temp.js +1 -1
- package/dist/test-hooks.d.ts +7 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -1
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +54 -8
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +5 -0
- package/dist/windows-path-alias.d.ts +39 -0
- package/dist/windows-path-alias.d.ts.map +1 -0
- package/dist/windows-path-alias.js +153 -0
- package/docs/advanced.md +25 -0
- package/docs/archive.md +23 -2
- package/docs/atomic.md +27 -3
- package/docs/copy.md +3 -0
- package/docs/durability.md +14 -0
- package/docs/errors.md +14 -6
- package/docs/file-store.md +24 -3
- package/docs/filename.md +14 -7
- package/docs/install-path.md +2 -2
- package/docs/install.md +8 -7
- package/docs/json-store.md +5 -1
- package/docs/json.md +11 -0
- package/docs/mutation-policy-proof.md +65 -0
- package/docs/native-helper.md +26 -9
- package/docs/native.md +32 -8
- package/docs/output.md +33 -11
- package/docs/path-prefix.md +64 -0
- package/docs/path-suffix-aliases.md +159 -0
- package/docs/path.md +11 -0
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +9 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +18 -3
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +6 -2
- package/docs/security-model.md +75 -8
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +5 -1
- package/docs/temp.md +260 -36
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
package/dist/pinned-write.js
CHANGED
|
@@ -4,7 +4,7 @@ import fsSync, {} from "node:fs";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
7
|
-
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
7
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
8
|
import { FsSafeError } from "./errors.js";
|
|
9
9
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
10
10
|
import { sameFileIdentity, sha256Hex } from "./file-identity.js";
|
|
@@ -21,6 +21,7 @@ import { writeAllToFile } from "./write-file-handle.js";
|
|
|
21
21
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
22
22
|
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
23
23
|
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
24
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
24
25
|
function byteLength(input, encoding) {
|
|
25
26
|
return typeof input === "string"
|
|
26
27
|
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
@@ -31,6 +32,7 @@ function assertSafeBasename(basename) {
|
|
|
31
32
|
basename === "." ||
|
|
32
33
|
basename === ".." ||
|
|
33
34
|
basename.includes("/") ||
|
|
35
|
+
(process.platform === "win32" && basename.includes("\\")) ||
|
|
34
36
|
basename.includes("\0")) {
|
|
35
37
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
36
38
|
}
|
|
@@ -40,6 +42,10 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
|
|
40
42
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
function fastParentGuardDeopt(error) {
|
|
46
|
+
return error instanceof FsSafeError &&
|
|
47
|
+
(error.code === "path-mismatch" || error.code === "not-file");
|
|
48
|
+
}
|
|
43
49
|
async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
|
|
44
50
|
let bytes = 0;
|
|
45
51
|
for await (const chunk of stream) {
|
|
@@ -49,12 +55,54 @@ async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutatio
|
|
|
49
55
|
await writeAllToFile(handle, buffer, { assertBeforeMutation });
|
|
50
56
|
}
|
|
51
57
|
}
|
|
58
|
+
const PINNED_WRITE_SNAPSHOT_KEYS = [
|
|
59
|
+
"rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
|
|
60
|
+
];
|
|
61
|
+
const RENAME_POLICY_SNAPSHOT_KEYS = ["targetPath", "renameIdentity"];
|
|
62
|
+
function copyOwnEnumerableExcept(source, excluded) {
|
|
63
|
+
const owned = {};
|
|
64
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
65
|
+
// Node 22's object-rest fast path can read excluded accessors eagerly.
|
|
66
|
+
// Exclude before even inspecting the descriptor so named authority and
|
|
67
|
+
// pathname fields remain single-read snapshots on every supported Node.
|
|
68
|
+
if (excluded.includes(key))
|
|
69
|
+
continue;
|
|
70
|
+
if (!Object.getOwnPropertyDescriptor(source, key)?.enumerable)
|
|
71
|
+
continue;
|
|
72
|
+
Object.defineProperty(owned, key, {
|
|
73
|
+
value: Reflect.get(source, key),
|
|
74
|
+
enumerable: true,
|
|
75
|
+
writable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return owned;
|
|
80
|
+
}
|
|
52
81
|
export async function runPinnedWriteHelper(params) {
|
|
53
|
-
const
|
|
54
|
-
|
|
82
|
+
const { rootPath, relativeParentPath, basename, maxBytes, rootIdentity } = params;
|
|
83
|
+
const ownedParams = copyOwnEnumerableExcept(params, PINNED_WRITE_SNAPSHOT_KEYS);
|
|
84
|
+
const normalizedParams = {
|
|
85
|
+
...ownedParams,
|
|
86
|
+
rootPath,
|
|
87
|
+
relativeParentPath,
|
|
88
|
+
basename,
|
|
89
|
+
maxBytes: normalizeMaxBytes(maxBytes),
|
|
90
|
+
rootIdentity,
|
|
91
|
+
};
|
|
92
|
+
assertSafeBasename(normalizedParams.basename);
|
|
55
93
|
validatePinnedOperationPayload({
|
|
56
|
-
relativeParentPath:
|
|
94
|
+
relativeParentPath: normalizedParams.relativeParentPath,
|
|
57
95
|
});
|
|
96
|
+
assertNoWindowsPathAlias(normalizedParams.rootPath, "filesystem", "pinned write root uses a Windows filesystem namespace alias");
|
|
97
|
+
assertNoWindowsPathAlias(normalizedParams.relativeParentPath, "relative", "pinned write parent uses a Windows filesystem namespace alias");
|
|
98
|
+
assertNoWindowsPathAlias(normalizedParams.basename, "relative", "pinned write basename uses a Windows filesystem namespace alias");
|
|
99
|
+
if (rootIdentity !== undefined) {
|
|
100
|
+
normalizedParams.rootIdentity = { dev: rootIdentity.dev, ino: rootIdentity.ino };
|
|
101
|
+
}
|
|
102
|
+
if (normalizedParams.mutationAdmission) {
|
|
103
|
+
const targetPath = path.join(normalizedParams.rootPath, ...normalizedParams.relativeParentPath.split("/").filter(Boolean), normalizedParams.basename);
|
|
104
|
+
await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(targetPath);
|
|
105
|
+
}
|
|
58
106
|
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
59
107
|
// content verification can replace the strict post-rename inode check.
|
|
60
108
|
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
@@ -67,51 +115,137 @@ export async function runPinnedWriteHelper(params) {
|
|
|
67
115
|
return await runPinnedWriteFallback(normalizedParams);
|
|
68
116
|
}
|
|
69
117
|
export async function runPinnedWriteWithRenamePolicy(params) {
|
|
70
|
-
const { targetPath, renameIdentity
|
|
118
|
+
const { targetPath, renameIdentity } = params;
|
|
119
|
+
const writeParams = copyOwnEnumerableExcept(params, RENAME_POLICY_SNAPSHOT_KEYS);
|
|
71
120
|
if (renameIdentity !== "verify-content-with-lock") {
|
|
72
121
|
return await runPinnedWriteHelper(writeParams);
|
|
73
122
|
}
|
|
74
123
|
const relativeTargetPath = writeParams.relativeParentPath
|
|
75
124
|
? `${writeParams.relativeParentPath}/${writeParams.basename}`
|
|
76
125
|
: writeParams.basename;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
return await withPinnedWriteRenameIdentityLock({
|
|
127
|
+
rootPath: writeParams.rootPath, targetPath, relativeTargetPath,
|
|
128
|
+
}, async () => await runPinnedWriteHelper({
|
|
129
|
+
...writeParams,
|
|
130
|
+
onRenameIdentityMismatch: "verify-content",
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
export async function withPinnedWriteRenameIdentityLock(params, run) {
|
|
134
|
+
const lockPath = path.join(params.rootPath, `.fs-safe-write-${sha256Hex(params.relativeTargetPath)}.lock`);
|
|
135
|
+
return await withSidecarLock(params.rootPath, {
|
|
136
|
+
managerKey: `fs-safe.write:${params.targetPath}`,
|
|
80
137
|
lockPath,
|
|
81
138
|
staleMs: 30_000,
|
|
82
139
|
timeoutMs: 5_000,
|
|
83
140
|
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
84
141
|
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
85
|
-
},
|
|
86
|
-
...writeParams,
|
|
87
|
-
onRenameIdentityMismatch: "verify-content",
|
|
88
|
-
}));
|
|
142
|
+
}, run);
|
|
89
143
|
}
|
|
90
144
|
async function runPinnedWriteFallback(params) {
|
|
91
145
|
const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
|
|
92
146
|
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
147
|
+
const mutationAdmission = params.mutationAdmission;
|
|
93
148
|
if (exactRoot)
|
|
94
149
|
await inspectDirectoryIdentity(params.rootPath, exactRoot);
|
|
95
150
|
let parentPath = params.relativeParentPath
|
|
96
151
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
97
152
|
: params.rootPath;
|
|
98
|
-
|
|
153
|
+
let parentGuard;
|
|
154
|
+
let parentAdmitted = false;
|
|
155
|
+
const initialTargetPath = path.join(parentPath, params.basename);
|
|
156
|
+
const retainedTargetPath = mutationAdmission?.beginParentWalk?.();
|
|
157
|
+
const ordinaryRetainedTarget = retainedTargetPath !== undefined &&
|
|
158
|
+
path.relative(path.resolve(retainedTargetPath), path.resolve(initialTargetPath)) === "";
|
|
159
|
+
let fastGuardClassifierFailure;
|
|
160
|
+
if (ordinaryRetainedTarget) {
|
|
161
|
+
try {
|
|
162
|
+
parentGuard = await createAsyncDirectoryGuard(parentPath, { bigint: true });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// Only a genuinely missing complete parent may enter the component
|
|
166
|
+
// creator. ENOTDIR, permission, identity, and canonicalization failures
|
|
167
|
+
// retain their original fail-closed result.
|
|
168
|
+
if (error?.code !== "ENOENT") {
|
|
169
|
+
if (!fastParentGuardDeopt(error))
|
|
170
|
+
throw error;
|
|
171
|
+
fastGuardClassifierFailure = error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (parentGuard) {
|
|
175
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
176
|
+
targetPath: initialTargetPath,
|
|
177
|
+
mutationPath: initialTargetPath,
|
|
178
|
+
phase: "parent",
|
|
179
|
+
}));
|
|
180
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
181
|
+
parentAdmitted = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (fastGuardClassifierFailure) {
|
|
185
|
+
// The optimized exact-guard classifier runs before policy admission. Let
|
|
186
|
+
// the established resolver restore deny/symlink precedence before using
|
|
187
|
+
// the guarded walker or reporting the classifier failure. Because this
|
|
188
|
+
// was not an ENOENT probe, it may never authorize a newly missing parent.
|
|
189
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
190
|
+
targetPath: initialTargetPath,
|
|
191
|
+
mutationPath: initialTargetPath,
|
|
192
|
+
phase: "parent",
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
if (params.mkdir && !parentGuard) {
|
|
99
196
|
// mkdirPathComponentsWithGuards may resolve the final component through
|
|
100
197
|
// an in-root symlink (e.g. a skill-bank layout). Use its returned real
|
|
101
198
|
// path for the subsequent guard and target path so we don't re-check the
|
|
102
199
|
// original, possibly-symlinked, lexical path and reject it outright.
|
|
103
|
-
|
|
200
|
+
const mkdirParams = {
|
|
104
201
|
rootReal: params.rootPath,
|
|
105
202
|
targetPath: parentPath,
|
|
106
203
|
rootIdentity: params.rootIdentity,
|
|
107
204
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
108
|
-
beforeComponent: async (componentPath) =>
|
|
109
|
-
|
|
205
|
+
beforeComponent: async (componentPath) => {
|
|
206
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
parentPath = await mkdirPathComponentsWithGuards(mutationAdmission ? {
|
|
210
|
+
...mkdirParams,
|
|
211
|
+
rejectSymlinks: mutationAdmission.rejectParentSymlinks,
|
|
212
|
+
revalidateParentAfterBeforeComponent: true,
|
|
213
|
+
synchronousAuthorizationIncludesFence: true,
|
|
214
|
+
retainedTargetPath,
|
|
215
|
+
afterCreateComponent: mutationAdmission.advanceCreatedDirectory,
|
|
216
|
+
beforeCreateComponent: (componentPath, prospectiveParentPath, retainedTarget, parent) => {
|
|
217
|
+
if (fastGuardClassifierFailure)
|
|
218
|
+
throw fastGuardClassifierFailure;
|
|
219
|
+
const request = Object.freeze({
|
|
220
|
+
targetPath: retainedTarget ?? path.join(prospectiveParentPath, params.basename),
|
|
221
|
+
mutationPath: componentPath,
|
|
222
|
+
phase: "parent-create",
|
|
223
|
+
});
|
|
224
|
+
return mutationAdmission.tryAuthorizeAtParent?.(request, parent) ??
|
|
225
|
+
mutationAdmission.authorize(request);
|
|
226
|
+
},
|
|
227
|
+
beforeUseComponent: async (_componentPath, prospectiveParentPath, retainedTarget) => {
|
|
228
|
+
const targetPath = retainedTarget ?? path.join(prospectiveParentPath, params.basename);
|
|
229
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
230
|
+
targetPath,
|
|
231
|
+
mutationPath: targetPath,
|
|
232
|
+
phase: "parent",
|
|
233
|
+
}));
|
|
234
|
+
},
|
|
235
|
+
} : mkdirParams);
|
|
110
236
|
}
|
|
111
|
-
|
|
237
|
+
parentGuard ??= params.mkdir
|
|
112
238
|
? await createAsyncDirectoryGuard(parentPath, { bigint: true })
|
|
113
239
|
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
|
|
114
240
|
const targetPath = path.join(parentPath, params.basename);
|
|
241
|
+
if (mutationAdmission && !parentAdmitted) {
|
|
242
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
243
|
+
targetPath,
|
|
244
|
+
mutationPath: targetPath,
|
|
245
|
+
phase: "parent",
|
|
246
|
+
}));
|
|
247
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
248
|
+
}
|
|
115
249
|
if (params.overwrite === false && (params.input.kind === "buffer" ||
|
|
116
250
|
(params.input.kind === "stream" && !params.input.stageBeforePublish))) {
|
|
117
251
|
const assertBeforeMutation = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { getNativeBinding } from "./native.js";
|
|
3
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
3
4
|
export async function createPrivateDirectory(targetPath, options) {
|
|
4
5
|
const platform = options?.platform ?? process.platform;
|
|
5
6
|
if (platform !== "win32") {
|
|
6
7
|
throw new FsSafeError("helper-unavailable", "private-directory creation is supported only on Windows");
|
|
7
8
|
}
|
|
9
|
+
assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
|
|
8
10
|
const native = getNativeBinding();
|
|
9
11
|
if (!native) {
|
|
10
12
|
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the matching optional native platform package; " +
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import { type FileHandle } from "node:fs/promises";
|
|
3
|
+
import { type TempPathRegistration } from "./temp-cleanup.js";
|
|
4
|
+
export type PrivateProducerHandoff = {
|
|
5
|
+
handle: FileHandle;
|
|
6
|
+
identity: BigIntStats;
|
|
7
|
+
unregister: TempPathRegistration;
|
|
8
|
+
};
|
|
9
|
+
export declare function handoffPrivateProducerFile(params: {
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
targetPath: string;
|
|
12
|
+
assertSourceParent: () => void;
|
|
13
|
+
assertTargetParent: () => void;
|
|
14
|
+
readWrite: boolean;
|
|
15
|
+
}): Promise<PrivateProducerHandoff>;
|
|
16
|
+
//# sourceMappingURL=private-producer-handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-producer-handoff.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,oBAAoB,CAAC;CAClC,CAAC;AA2OF,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA6HlC"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import fs, {} from "node:fs/promises";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
7
|
+
import { registerTempPathForExit, } from "./temp-cleanup.js";
|
|
8
|
+
function pathMismatch(message) {
|
|
9
|
+
return new FsSafeError("path-mismatch", message);
|
|
10
|
+
}
|
|
11
|
+
function assertInitialSource(stat) {
|
|
12
|
+
if (stat.isSymbolicLink()) {
|
|
13
|
+
throw new FsSafeError("path-alias", "isolated producer output must not be a symlink");
|
|
14
|
+
}
|
|
15
|
+
if (!stat.isFile()) {
|
|
16
|
+
throw new FsSafeError("not-file", "isolated producer output must be a regular file");
|
|
17
|
+
}
|
|
18
|
+
if (stat.nlink !== 1n) {
|
|
19
|
+
throw new FsSafeError("hardlink", "isolated producer output must have exactly one link");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function inspectLinkedFile(inspect, expected, links, label) {
|
|
23
|
+
return inspectFileIdentitySync(() => {
|
|
24
|
+
const stat = inspect();
|
|
25
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== links) {
|
|
26
|
+
throw pathMismatch(`${label} changed during isolated producer handoff`);
|
|
27
|
+
}
|
|
28
|
+
return stat;
|
|
29
|
+
}, expected);
|
|
30
|
+
}
|
|
31
|
+
function assertParents(params) {
|
|
32
|
+
params.assertTargetParent();
|
|
33
|
+
params.assertSourceParent();
|
|
34
|
+
}
|
|
35
|
+
function normalizeLinkError(error) {
|
|
36
|
+
const code = error?.code;
|
|
37
|
+
if (code === "EEXIST") {
|
|
38
|
+
return new FsSafeError("already-exists", "isolated producer sibling already exists", {
|
|
39
|
+
cause: error instanceof Error ? error : undefined,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (code === "EXDEV" ||
|
|
43
|
+
code === "ENOSYS" ||
|
|
44
|
+
code === "ENOTSUP" ||
|
|
45
|
+
code === "EOPNOTSUPP" ||
|
|
46
|
+
code === "EPERM") {
|
|
47
|
+
return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", {
|
|
48
|
+
cause: error instanceof Error ? error : undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return error;
|
|
52
|
+
}
|
|
53
|
+
function removeSiblingIfOwned(params) {
|
|
54
|
+
if (!params.state.siblingOwned)
|
|
55
|
+
return;
|
|
56
|
+
params.assertTargetParent();
|
|
57
|
+
let current;
|
|
58
|
+
try {
|
|
59
|
+
current = fsSync.lstatSync(params.targetPath, { bigint: true });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error?.code === "ENOENT") {
|
|
63
|
+
params.state.siblingOwned = false;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
if (current.isSymbolicLink() ||
|
|
69
|
+
!current.isFile() ||
|
|
70
|
+
(current.nlink !== 1n && current.nlink !== 2n) ||
|
|
71
|
+
!sameFileIdentityForCleanup(current, params.identity)) {
|
|
72
|
+
throw pathMismatch("isolated producer sibling changed before cleanup");
|
|
73
|
+
}
|
|
74
|
+
fsSync.unlinkSync(params.targetPath);
|
|
75
|
+
params.state.siblingOwned = false;
|
|
76
|
+
params.assertTargetParent();
|
|
77
|
+
}
|
|
78
|
+
function combinedFailure(primary, settlement) {
|
|
79
|
+
if (settlement.length === 0)
|
|
80
|
+
return primary;
|
|
81
|
+
return new AggregateError([primary, ...settlement], "isolated producer handoff and settlement failed");
|
|
82
|
+
}
|
|
83
|
+
function hasCode(error, codes) {
|
|
84
|
+
return !(error instanceof FsSafeError) &&
|
|
85
|
+
codes.includes(error?.code ?? "");
|
|
86
|
+
}
|
|
87
|
+
async function throwAfterClosing(primary, handles) {
|
|
88
|
+
const settlement = [];
|
|
89
|
+
const closed = new Set();
|
|
90
|
+
for (const handle of handles) {
|
|
91
|
+
if (!handle || closed.has(handle))
|
|
92
|
+
continue;
|
|
93
|
+
closed.add(handle);
|
|
94
|
+
try {
|
|
95
|
+
await handle.close();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
settlement.push(error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw combinedFailure(primary, settlement);
|
|
102
|
+
}
|
|
103
|
+
function inspectSourceDescriptor(handle, identity, links = 1n) {
|
|
104
|
+
inspectLinkedFile(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity, links, "isolated producer descriptor");
|
|
105
|
+
}
|
|
106
|
+
function historicalOpenFlags(readWrite) {
|
|
107
|
+
const access = readWrite ? fsSync.constants.O_RDWR : fsSync.constants.O_RDONLY;
|
|
108
|
+
return access | resolveReadOpenFlags();
|
|
109
|
+
}
|
|
110
|
+
async function openHistoricalSource(sourcePath, identity, readWrite, links) {
|
|
111
|
+
let handle;
|
|
112
|
+
try {
|
|
113
|
+
handle = await fs.open(sourcePath, historicalOpenFlags(readWrite));
|
|
114
|
+
inspectSourceDescriptor(handle, identity, links);
|
|
115
|
+
return handle;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (handle)
|
|
119
|
+
return await throwAfterClosing(error, [handle]);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Windows data-read access can synchronously inspect a completed file through
|
|
124
|
+
// filesystem filters. Prefer write-only admission, which still supports the
|
|
125
|
+
// descriptor metadata, chmod and sync operations used below. Providers or
|
|
126
|
+
// read-only files that reject that access retain the historical open contract.
|
|
127
|
+
async function openProducerSource(params, identity, links = 1n) {
|
|
128
|
+
if (process.platform !== "win32") {
|
|
129
|
+
return await openHistoricalSource(params.sourcePath, identity, params.readWrite, links);
|
|
130
|
+
}
|
|
131
|
+
let provisional;
|
|
132
|
+
try {
|
|
133
|
+
provisional = await fs.open(params.sourcePath, fsSync.constants.O_WRONLY);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!hasCode(error, ["EACCES", "EPERM", "EBUSY"]))
|
|
137
|
+
throw error;
|
|
138
|
+
return await openHistoricalSource(params.sourcePath, identity, params.readWrite, links);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
inspectSourceDescriptor(provisional, identity, links);
|
|
142
|
+
return provisional;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (!hasCode(error, ["EACCES", "EPERM"])) {
|
|
146
|
+
return await throwAfterClosing(error, [provisional]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
let fallback;
|
|
150
|
+
try {
|
|
151
|
+
fallback = await fs.open(params.sourcePath, historicalOpenFlags(params.readWrite));
|
|
152
|
+
inspectSourceDescriptor(fallback, identity, links);
|
|
153
|
+
inspectLinkedFile(() => fsSync.lstatSync(params.sourcePath, { bigint: true }), identity, links, "isolated producer path");
|
|
154
|
+
assertParents(params);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return await throwAfterClosing(error, [fallback, provisional]);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await provisional.close();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return await throwAfterClosing(error, [fallback]);
|
|
164
|
+
}
|
|
165
|
+
return fallback;
|
|
166
|
+
}
|
|
167
|
+
function assertLinkedHandoff(params, handle, identity) {
|
|
168
|
+
assertParents(params);
|
|
169
|
+
inspectSourceDescriptor(handle, identity, 2n);
|
|
170
|
+
inspectLinkedFile(() => fsSync.lstatSync(params.sourcePath, { bigint: true }), identity, 2n, "isolated producer path");
|
|
171
|
+
inspectLinkedFile(() => fsSync.lstatSync(params.targetPath, { bigint: true }), identity, 2n, "isolated producer sibling");
|
|
172
|
+
}
|
|
173
|
+
export async function handoffPrivateProducerFile(params) {
|
|
174
|
+
assertParents(params);
|
|
175
|
+
const identity = inspectFileIdentitySync(() => {
|
|
176
|
+
const stat = fsSync.lstatSync(params.sourcePath, { bigint: true });
|
|
177
|
+
assertInitialSource(stat);
|
|
178
|
+
return stat;
|
|
179
|
+
});
|
|
180
|
+
let handle;
|
|
181
|
+
let siblingHandle;
|
|
182
|
+
let unregister;
|
|
183
|
+
const state = { siblingOwned: false };
|
|
184
|
+
try {
|
|
185
|
+
handle = await openProducerSource(params, identity);
|
|
186
|
+
inspectLinkedFile(() => fsSync.lstatSync(params.sourcePath, { bigint: true }), identity, 1n, "isolated producer path");
|
|
187
|
+
assertParents(params);
|
|
188
|
+
unregister = registerTempPathForExit(params.targetPath, {
|
|
189
|
+
cleanupSync: () => removeSiblingIfOwned({
|
|
190
|
+
assertTargetParent: params.assertTargetParent,
|
|
191
|
+
identity,
|
|
192
|
+
state,
|
|
193
|
+
targetPath: params.targetPath,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
assertParents(params);
|
|
197
|
+
try {
|
|
198
|
+
fsSync.linkSync(params.sourcePath, params.targetPath);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
throw normalizeLinkError(error);
|
|
202
|
+
}
|
|
203
|
+
state.siblingOwned = true;
|
|
204
|
+
assertLinkedHandoff(params, handle, identity);
|
|
205
|
+
if (process.platform === "win32") {
|
|
206
|
+
// Legacy Windows unlink retains a delete-pending name until handles
|
|
207
|
+
// opened through that name close. Transfer the pin before retiring it.
|
|
208
|
+
siblingHandle = await openProducerSource({ ...params, sourcePath: params.targetPath }, identity, 2n);
|
|
209
|
+
assertLinkedHandoff(params, siblingHandle, identity);
|
|
210
|
+
inspectSourceDescriptor(handle, identity, 2n);
|
|
211
|
+
await handle.close();
|
|
212
|
+
handle = siblingHandle;
|
|
213
|
+
siblingHandle = undefined;
|
|
214
|
+
// The old handle's close yielded; both names must still be ours.
|
|
215
|
+
assertLinkedHandoff(params, handle, identity);
|
|
216
|
+
}
|
|
217
|
+
assertParents(params);
|
|
218
|
+
fsSync.unlinkSync(params.sourcePath);
|
|
219
|
+
assertParents(params);
|
|
220
|
+
const opened = inspectLinkedFile(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity, 1n, "isolated producer descriptor");
|
|
221
|
+
const sibling = inspectLinkedFile(() => fsSync.lstatSync(params.targetPath, { bigint: true }), identity, 1n, "isolated producer sibling");
|
|
222
|
+
const originalMode = identity.mode & 511n;
|
|
223
|
+
if (process.platform === "win32" && (originalMode & 128n) === 0n &&
|
|
224
|
+
((opened.mode & 511n) !== originalMode || (sibling.mode & 511n) !== originalMode)) {
|
|
225
|
+
// Legacy unlink clears the shared read-only attribute. Repair only the
|
|
226
|
+
// admitted sibling inode, without reopening or chmodding its pathname.
|
|
227
|
+
assertParents(params);
|
|
228
|
+
fsSync.fchmodSync(handle.fd, Number(originalMode));
|
|
229
|
+
assertParents(params);
|
|
230
|
+
const restored = inspectLinkedFile(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity, 1n, "isolated producer descriptor");
|
|
231
|
+
const restoredSibling = inspectLinkedFile(() => fsSync.lstatSync(params.targetPath, { bigint: true }), identity, 1n, "isolated producer sibling");
|
|
232
|
+
if ((restored.mode & 511n) !== originalMode || (restoredSibling.mode & 511n) !== originalMode) {
|
|
233
|
+
throw pathMismatch("isolated producer read-only mode could not be restored");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const registered = unregister;
|
|
237
|
+
const release = (() => {
|
|
238
|
+
state.siblingOwned = false;
|
|
239
|
+
registered();
|
|
240
|
+
});
|
|
241
|
+
release.setIdentity = registered.setIdentity;
|
|
242
|
+
return { handle, identity, unregister: release };
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const settlement = [];
|
|
246
|
+
if (unregister) {
|
|
247
|
+
try {
|
|
248
|
+
removeSiblingIfOwned({
|
|
249
|
+
assertTargetParent: params.assertTargetParent,
|
|
250
|
+
identity,
|
|
251
|
+
state,
|
|
252
|
+
targetPath: params.targetPath,
|
|
253
|
+
});
|
|
254
|
+
unregister();
|
|
255
|
+
}
|
|
256
|
+
catch (cleanupError) {
|
|
257
|
+
settlement.push(cleanupError);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const retained of new Set([siblingHandle, handle])) {
|
|
261
|
+
if (!retained)
|
|
262
|
+
continue;
|
|
263
|
+
try {
|
|
264
|
+
await retained.close();
|
|
265
|
+
}
|
|
266
|
+
catch (closeError) {
|
|
267
|
+
settlement.push(closeError);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw combinedFailure(error, settlement);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -1,43 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type TempPathIdentityReceipt } from "./temp-cleanup.js";
|
|
3
|
-
import { type TempWorkspaceCleanupResult, type TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
|
|
1
|
+
import type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
|
|
4
2
|
export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
|
|
5
|
-
export type TempWorkspaceOptions
|
|
6
|
-
rootDir: string;
|
|
7
|
-
prefix: string;
|
|
8
|
-
dirMode?: number;
|
|
9
|
-
mode?: number;
|
|
10
|
-
cleanupSafety?: TempWorkspaceCleanupSafety;
|
|
11
|
-
};
|
|
12
|
-
export type TempWorkspace = {
|
|
13
|
-
dir: string;
|
|
14
|
-
identity: TempPathIdentityReceipt;
|
|
15
|
-
store: FileStore;
|
|
16
|
-
path(fileName: string): string;
|
|
17
|
-
write(fileName: string, data: string | Uint8Array): Promise<string>;
|
|
18
|
-
writeText(fileName: string, data: string): Promise<string>;
|
|
19
|
-
writeJson(fileName: string, data: unknown, options?: {
|
|
20
|
-
trailingNewline?: boolean;
|
|
21
|
-
}): Promise<string>;
|
|
22
|
-
copyIn(fileName: string, sourcePath: string): Promise<string>;
|
|
23
|
-
read(fileName: string): Promise<Buffer>;
|
|
24
|
-
cleanup(): Promise<TempWorkspaceCleanupResult>;
|
|
25
|
-
[Symbol.asyncDispose](): Promise<void>;
|
|
26
|
-
};
|
|
27
|
-
export type TempWorkspaceSync = {
|
|
28
|
-
dir: string;
|
|
29
|
-
identity: TempPathIdentityReceipt;
|
|
30
|
-
store: FileStoreSync;
|
|
31
|
-
path(fileName: string): string;
|
|
32
|
-
write(fileName: string, data: string | Uint8Array): string;
|
|
33
|
-
writeText(fileName: string, data: string): string;
|
|
34
|
-
writeJson(fileName: string, data: unknown, options?: {
|
|
35
|
-
trailingNewline?: boolean;
|
|
36
|
-
}): string;
|
|
37
|
-
read(fileName: string): Buffer;
|
|
38
|
-
cleanup(): TempWorkspaceCleanupResult;
|
|
39
|
-
[Symbol.dispose](): void;
|
|
40
|
-
};
|
|
3
|
+
export type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
|
|
41
4
|
export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace>;
|
|
42
5
|
export declare function withTempWorkspace<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspace) => Promise<T>): Promise<T>;
|
|
43
6
|
export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAExG,YAAY,EACV,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AA8OxG,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAOZ;AA6JD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAOH"}
|