@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/root-impl.js
CHANGED
|
@@ -56,28 +56,30 @@ import fs from "node:fs/promises";
|
|
|
56
56
|
import path from "node:path";
|
|
57
57
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
58
58
|
import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
|
|
59
|
-
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
59
|
+
import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
60
60
|
import { FsSafeError } from "./errors.js";
|
|
61
61
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
62
|
-
import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
|
|
63
|
-
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
64
62
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
63
|
+
import { openLocalFileDescriptor } from "./local-file-descriptor.js";
|
|
64
|
+
import { assertMutationNotDenied, mergeDenyMutationPolicies } from "./deny-mutations.js";
|
|
65
|
+
import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
|
|
66
|
+
import { openedPathResolutionError, recordExclusiveCreateFailure, recordOpenedFileFailure } from "./opened-file-failure.js";
|
|
67
|
+
import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy } from "./pinned-write.js";
|
|
68
|
+
import { preparePinnedWriteMutationAdmission, snapshotPinnedMutationPolicy } from "./pinned-mutation-admission.js";
|
|
69
69
|
import { getNativeBinding } from "./native.js";
|
|
70
70
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
71
|
-
import {
|
|
72
|
-
import { assertNoNulPathInput,
|
|
71
|
+
import { PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
72
|
+
import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError, } from "./path.js";
|
|
73
73
|
import { readOpenedFileSafely } from "./read-opened-file.js";
|
|
74
|
-
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
75
|
-
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
74
|
+
import { cleanupPinnedFilePath, removePathIfIdentityUnchanged } from "./replace-file-temp-owner.js";
|
|
76
75
|
import { realpathSync } from "./realpath.js";
|
|
76
|
+
import { mkdirPathFallback, prepareRootWriteTarget, tryMkdirAtExactParent } from "./root-directory-creation.js";
|
|
77
77
|
import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
|
|
78
|
-
import { resolveRootPath } from "./root-path.js";
|
|
78
|
+
import { resolveRootPath, resolveRootPathForRemoval } from "./root-path.js";
|
|
79
|
+
import { RemovalPathReceipts } from "./root-remove-receipt.js";
|
|
79
80
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
80
|
-
import {
|
|
81
|
+
import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-list.js";
|
|
82
|
+
import { statResolvedPathInRoot } from "./root-path-stat.js";
|
|
81
83
|
import { entriesInRoot } from "./root-entries.js";
|
|
82
84
|
import { assertMoveMutationAllowed } from "./root-move-preflight.js";
|
|
83
85
|
import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
@@ -89,12 +91,19 @@ import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
|
89
91
|
import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
|
|
90
92
|
import { serializePathWrite } from "./write-queue.js";
|
|
91
93
|
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
92
|
-
import {
|
|
93
|
-
import {
|
|
94
|
+
import { assertRootWritePathSelectionSync, assertRootWriteSelectionSync, createRootWriteSelectionForFd, prepareGuardedRootWritePathSelection, resolveGuardedWritePathInRoot, resolveGuardedWriteTargetInRoot, resolvePinnedWriteTargetInRoot, refreshRetainedRootWriteAdmission, refreshRootWritePathSelection, retainRootWriteSelection, takeRootWriteSelection, } from "./root-write-admission.js";
|
|
95
|
+
import { prepareSharedRootWriteTarget } from "./root-write-complete-parent.js";
|
|
96
|
+
import { finishRootFallbackWrite } from "./root-write-publication.js";
|
|
97
|
+
import { assertRootFallbackWritePath, withRootFallbackCompatibilityLock } from "./root-write-compatibility.js";
|
|
98
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
99
|
+
import { movePathNoReplaceNative } from "./root-move-noreplace.js";
|
|
100
|
+
import { admitRootReadHandle, inspectOpenedPathIdentitySync } from "./root-read-admission.js";
|
|
94
101
|
import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
|
|
95
102
|
import { writeAllToFile } from "./write-file-handle.js";
|
|
96
103
|
import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
|
|
97
104
|
import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
|
|
105
|
+
import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot } from "./windows-path-alias.js";
|
|
106
|
+
import { resolvePinnedObservedPathInRoot } from "./root-observed-path.js";
|
|
98
107
|
import { mergeReadOptions, readDefaults, } from "./root-options.js";
|
|
99
108
|
import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
|
|
100
109
|
export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
|
|
@@ -105,8 +114,6 @@ function logWarn(message) {
|
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
108
|
-
const OPEN_READ_FLAGS = resolveReadOpenFlags();
|
|
109
|
-
const OPEN_READ_FOLLOW_FLAGS = resolveReadOpenFlags({ followSymlinks: true });
|
|
110
117
|
const OPEN_WRITE_EXISTING_FLAGS = fsConstants.O_WRONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0) |
|
|
111
118
|
resolveNonblockingWriteFlag();
|
|
112
119
|
const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
|
|
@@ -130,69 +137,9 @@ function openResult(params) {
|
|
|
130
137
|
};
|
|
131
138
|
}
|
|
132
139
|
async function openVerifiedLocalFile(filePath, options) {
|
|
133
|
-
assertNoUnsafeDeviceReadPath(filePath);
|
|
134
140
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
135
|
-
|
|
136
|
-
let observedBeforeOpen = false;
|
|
137
|
-
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
138
|
-
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
141
|
+
const { handle, stat, identity, preOpenStat } = await openLocalFileDescriptor(filePath, options);
|
|
139
142
|
try {
|
|
140
|
-
preOpenStat = await inspectFileIdentity(async () => {
|
|
141
|
-
const stat = fsSync.lstatSync(filePath, { bigint: true });
|
|
142
|
-
observedBeforeOpen = true;
|
|
143
|
-
if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
144
|
-
throw new FsSafeError("symlink", "symlink not allowed");
|
|
145
|
-
}
|
|
146
|
-
if (!stat.isFile() && !stat.isSymbolicLink()) {
|
|
147
|
-
throw new FsSafeError("not-file", "not a file");
|
|
148
|
-
}
|
|
149
|
-
return stat;
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
if (err instanceof FsSafeError || observedBeforeOpen) {
|
|
154
|
-
throw err;
|
|
155
|
-
}
|
|
156
|
-
// Only an initial lookup failure falls through; a failed re-inspection aborts.
|
|
157
|
-
}
|
|
158
|
-
if (preOpenStat) {
|
|
159
|
-
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
160
|
-
}
|
|
161
|
-
const openFlags = options?.symlinks === "follow-within-root"
|
|
162
|
-
? OPEN_READ_FOLLOW_FLAGS
|
|
163
|
-
: OPEN_READ_FLAGS;
|
|
164
|
-
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
165
|
-
let handle;
|
|
166
|
-
try {
|
|
167
|
-
handle = await fs.open(filePath, openFlags).catch((error) => recordFileOpenFailure(isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
|
|
168
|
-
}
|
|
169
|
-
catch (err) {
|
|
170
|
-
if (isSymlinkOpenError(err)) {
|
|
171
|
-
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
172
|
-
}
|
|
173
|
-
// Defensive: if open still throws EISDIR (e.g. race), sanitize so it never leaks.
|
|
174
|
-
if (hasNodeErrorCode(err, "EISDIR")) {
|
|
175
|
-
throw new FsSafeError("not-file", "not a file");
|
|
176
|
-
}
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
181
|
-
const stat = fsSync.fstatSync(handle.fd);
|
|
182
|
-
if (!stat.isFile()) {
|
|
183
|
-
throw new FsSafeError("not-file", "not a file");
|
|
184
|
-
}
|
|
185
|
-
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
186
|
-
let openedIdentity;
|
|
187
|
-
const identity = await inspectFileIdentity(async () => (openedIdentity = fsSync.fstatSync(handle.fd, { bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
|
|
188
|
-
if ([0, 1].includes(stat.nlink)) {
|
|
189
|
-
await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
|
|
190
|
-
}
|
|
191
|
-
throw error;
|
|
192
|
-
});
|
|
193
|
-
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
194
|
-
throw hardlinkedPathNotAllowedError();
|
|
195
|
-
}
|
|
196
143
|
const inspectPathIdentity = async (inspect) => {
|
|
197
144
|
try {
|
|
198
145
|
return await inspectFileIdentity(inspect, identity);
|
|
@@ -205,15 +152,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
205
152
|
throw failure;
|
|
206
153
|
}
|
|
207
154
|
};
|
|
208
|
-
await inspectPathIdentity(async () =>
|
|
209
|
-
const pathStat = options?.symlinks === "follow-within-root"
|
|
210
|
-
? fsSync.statSync(filePath, { bigint: true })
|
|
211
|
-
: fsSync.lstatSync(filePath, { bigint: true });
|
|
212
|
-
if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
213
|
-
throw new FsSafeError("symlink", "symlink not allowed");
|
|
214
|
-
}
|
|
215
|
-
return pathStat;
|
|
216
|
-
});
|
|
155
|
+
await inspectPathIdentity(async () => inspectOpenedPathIdentitySync(filePath, options?.symlinks));
|
|
217
156
|
await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
|
|
218
157
|
const resolved = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
219
158
|
.catch(async (error) => {
|
|
@@ -223,6 +162,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
223
162
|
throw error;
|
|
224
163
|
});
|
|
225
164
|
const { realPath } = resolved;
|
|
165
|
+
assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
|
|
226
166
|
let resolvedStat = resolved.stat;
|
|
227
167
|
await inspectPathIdentity(async () => {
|
|
228
168
|
// Reuse the post-realpath observation; unknown Windows identities still
|
|
@@ -242,12 +182,14 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
242
182
|
}
|
|
243
183
|
}
|
|
244
184
|
export class RootHandle {
|
|
185
|
+
rootGuard;
|
|
245
186
|
rootIdentity;
|
|
246
187
|
rootDir;
|
|
247
188
|
rootReal;
|
|
248
189
|
rootWithSep;
|
|
249
190
|
defaults;
|
|
250
191
|
constructor(context, defaults = {}) {
|
|
192
|
+
this.rootGuard = context.rootGuard;
|
|
251
193
|
this.rootIdentity = context.rootIdentity;
|
|
252
194
|
this.rootDir = context.rootDir;
|
|
253
195
|
this.rootReal = context.rootReal;
|
|
@@ -257,6 +199,7 @@ export class RootHandle {
|
|
|
257
199
|
get context() {
|
|
258
200
|
return {
|
|
259
201
|
rootDir: this.rootDir,
|
|
202
|
+
rootGuard: this.rootGuard,
|
|
260
203
|
rootIdentity: this.rootIdentity,
|
|
261
204
|
rootReal: this.rootReal,
|
|
262
205
|
rootWithSep: this.rootWithSep,
|
|
@@ -311,7 +254,7 @@ export class RootHandle {
|
|
|
311
254
|
async openWritable(relativePath, options = {}) {
|
|
312
255
|
assertValidRootDestinationPath(relativePath);
|
|
313
256
|
const writeMode = options.writeMode ?? "replace";
|
|
314
|
-
|
|
257
|
+
const target = await openWritableFileInRoot(this.context, {
|
|
315
258
|
relativePath,
|
|
316
259
|
mkdir: this.defaults.mkdir,
|
|
317
260
|
mode: this.defaults.mode,
|
|
@@ -319,6 +262,7 @@ export class RootHandle {
|
|
|
319
262
|
append: writeMode === "append",
|
|
320
263
|
truncateExisting: writeMode === "replace",
|
|
321
264
|
}).catch(rethrowMutationAuthorityError);
|
|
265
|
+
return target.opened;
|
|
322
266
|
}
|
|
323
267
|
async append(relativePath, data, options = {}) {
|
|
324
268
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -430,6 +374,7 @@ export class RootHandle {
|
|
|
430
374
|
assertValidRootDestinationPath(toRelative);
|
|
431
375
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
432
376
|
const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
|
|
377
|
+
const overwrite = options.overwrite ?? false;
|
|
433
378
|
await assertMoveMutationAllowed(this.context, {
|
|
434
379
|
fromRelative,
|
|
435
380
|
toRelative,
|
|
@@ -440,7 +385,7 @@ export class RootHandle {
|
|
|
440
385
|
denyMutations,
|
|
441
386
|
assertBeforeMutation,
|
|
442
387
|
mutationSymlinks,
|
|
443
|
-
overwrite
|
|
388
|
+
overwrite,
|
|
444
389
|
toRelative,
|
|
445
390
|
}).catch(rethrowMutationAuthorityError);
|
|
446
391
|
}
|
|
@@ -473,7 +418,8 @@ export async function root(rootDir, defaults = {}) {
|
|
|
473
418
|
export function rootFromDirectoryGuard(guard, defaults = {}) {
|
|
474
419
|
normalizeMaxBytes(defaults.maxBytes);
|
|
475
420
|
return new RootHandle({
|
|
476
|
-
rootDir:
|
|
421
|
+
rootDir: resolvePathPreservingWindowsRoot(guard.dir),
|
|
422
|
+
rootGuard: { dir: guard.realPath, realPath: guard.realPath, stat: guard.stat },
|
|
477
423
|
rootIdentity: { dev: guard.stat.dev, ino: guard.stat.ino },
|
|
478
424
|
rootReal: guard.realPath,
|
|
479
425
|
rootWithSep: ensureTrailingSep(guard.realPath),
|
|
@@ -486,24 +432,22 @@ async function openFileInRoot(root, params) {
|
|
|
486
432
|
...readSymlinkResolution(params.symlinks),
|
|
487
433
|
resolveCanonical: true,
|
|
488
434
|
});
|
|
489
|
-
const
|
|
435
|
+
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
436
|
+
if (fsSafeTestHooks?.afterRootReadPathResolution) {
|
|
437
|
+
await fsSafeTestHooks.afterRootReadPathResolution(resolved);
|
|
438
|
+
}
|
|
439
|
+
const { handle, stat, identity } = await openLocalFileDescriptor(resolved, {
|
|
490
440
|
symlinks: params.symlinks,
|
|
491
441
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
rootIdentity: root.rootIdentity,
|
|
442
|
+
// The admission helper owns the descriptor until the complete root/file/root
|
|
443
|
+
// fence succeeds, then transfers that still-open handle to the caller.
|
|
444
|
+
const admitted = await admitRootReadHandle({
|
|
445
|
+
root, filePath: resolved, opened: { handle, stat }, identity,
|
|
446
|
+
hardlinks: params.hardlinks, symlinks: params.symlinks,
|
|
447
|
+
beforeFinalFence: fsSafeTestHooks?.beforeRootReadFinalFence,
|
|
448
|
+
afterPathIdentityCheck: fsSafeTestHooks?.afterRootReadFinalPathIdentityCheck,
|
|
500
449
|
});
|
|
501
|
-
|
|
502
|
-
await opened.handle.close().catch(() => { });
|
|
503
|
-
throw outsideWorkspaceError();
|
|
504
|
-
}
|
|
505
|
-
opened.realPath = admittedRealPath.path;
|
|
506
|
-
return opened;
|
|
450
|
+
return openResult(admitted);
|
|
507
451
|
}
|
|
508
452
|
async function readFileInRoot(root, params) {
|
|
509
453
|
const opened = await openFileInRoot(root, params);
|
|
@@ -534,8 +478,10 @@ export async function readLocalFileSafely(params) {
|
|
|
534
478
|
}
|
|
535
479
|
}
|
|
536
480
|
export async function openLocalFileSafely(params) {
|
|
537
|
-
|
|
538
|
-
|
|
481
|
+
const filePath = params.filePath;
|
|
482
|
+
assertNoNulPathInput(filePath, "file path contains a NUL byte");
|
|
483
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
|
|
484
|
+
return (await openVerifiedLocalFile(filePath)).opened;
|
|
539
485
|
}
|
|
540
486
|
function emitWriteBoundaryWarning(reason) {
|
|
541
487
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
@@ -546,50 +492,26 @@ function buildAtomicWriteTempPath(targetPath) {
|
|
|
546
492
|
function rootWriteQueueKey(root, relativePath) {
|
|
547
493
|
return `${root.rootReal}\0${relativePath}`;
|
|
548
494
|
}
|
|
549
|
-
async function prepareRootWriteTarget(root, targetPath, assertBeforeMutation) {
|
|
550
|
-
const parentPath = await mkdirPathComponentsWithGuards({
|
|
551
|
-
rootReal: root.rootReal,
|
|
552
|
-
rootIdentity: root.rootIdentity,
|
|
553
|
-
targetPath: path.dirname(targetPath),
|
|
554
|
-
assertBeforeMutation,
|
|
555
|
-
});
|
|
556
|
-
// Continue through the guarded walk's real parent instead of re-entering
|
|
557
|
-
// the original path through a symlinked component.
|
|
558
|
-
return path.join(parentPath, path.basename(targetPath));
|
|
559
|
-
}
|
|
560
|
-
async function resolveGuardedWritePathInRoot(root, params) {
|
|
561
|
-
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
562
|
-
aliasErrorCode: "path-alias",
|
|
563
|
-
rejectAmbiguousParents: true,
|
|
564
|
-
allowFinalSymlink: params.allowFinalSymlink,
|
|
565
|
-
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
566
|
-
});
|
|
567
|
-
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
568
|
-
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
569
|
-
try {
|
|
570
|
-
await assertNoPathAliasEscape({
|
|
571
|
-
absolutePath: resolvedPath.resolved,
|
|
572
|
-
rootPath: resolvedPath.rootReal,
|
|
573
|
-
boundaryLabel: "root",
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
catch (error) {
|
|
577
|
-
throw new FsSafeError("path-alias", "path alias escape blocked", {
|
|
578
|
-
cause: error instanceof Error ? error : undefined,
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return resolvedPath;
|
|
583
|
-
}
|
|
584
495
|
async function openWritableFileInRoot(root, params) {
|
|
585
|
-
const
|
|
496
|
+
const guardedTarget = params.denyMutations === undefined && params.mutationSymlinks === undefined
|
|
497
|
+
? undefined
|
|
498
|
+
: await resolveGuardedWriteTargetInRoot(root, {
|
|
499
|
+
relativePath: params.relativePath,
|
|
500
|
+
denyMutations: params.denyMutations,
|
|
501
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
502
|
+
});
|
|
503
|
+
const { resolved } = guardedTarget?.resolvedPath ?? await resolveGuardedWritePathInRoot(root, {
|
|
586
504
|
relativePath: params.relativePath,
|
|
587
|
-
denyMutations: params.denyMutations,
|
|
588
|
-
mutationSymlinks: params.mutationSymlinks,
|
|
589
505
|
});
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
:
|
|
506
|
+
const prepared = guardedTarget ? await prepareSharedRootWriteTarget(root, {
|
|
507
|
+
relativePath: params.relativePath, guardedTarget, mkdir: params.mkdir,
|
|
508
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
509
|
+
}) : undefined;
|
|
510
|
+
const mutationAdmission = prepared?.mutationAdmission;
|
|
511
|
+
const preparedParent = prepared?.preparedParent;
|
|
512
|
+
let ioPath = prepared?.targetPath ?? (params.mkdir === false ? resolved :
|
|
513
|
+
await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation));
|
|
514
|
+
const operationTargetPath = ioPath;
|
|
593
515
|
try {
|
|
594
516
|
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
595
517
|
const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
|
|
@@ -611,13 +533,20 @@ async function openWritableFileInRoot(root, params) {
|
|
|
611
533
|
throw err;
|
|
612
534
|
}
|
|
613
535
|
}
|
|
536
|
+
assertRootFallbackWritePath(params.expectedWritePath, ioPath);
|
|
614
537
|
const mode = params.mode ?? 0o600;
|
|
615
538
|
let handle;
|
|
616
539
|
let createdForWrite = false;
|
|
540
|
+
let writePathSelection = undefined;
|
|
617
541
|
const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
|
|
618
542
|
const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
|
|
619
543
|
try {
|
|
544
|
+
writePathSelection = guardedTarget
|
|
545
|
+
? await prepareGuardedRootWritePathSelection(guardedTarget, ioPath, operationTargetPath, preparedParent)
|
|
546
|
+
: undefined;
|
|
620
547
|
try {
|
|
548
|
+
if (writePathSelection)
|
|
549
|
+
assertRootWritePathSelectionSync(root, writePathSelection);
|
|
621
550
|
handle = await fs.open(ioPath, existingFlags, mode);
|
|
622
551
|
}
|
|
623
552
|
catch (err) {
|
|
@@ -627,7 +556,11 @@ async function openWritableFileInRoot(root, params) {
|
|
|
627
556
|
if (!isNotFoundPathError(err)) {
|
|
628
557
|
throw err;
|
|
629
558
|
}
|
|
559
|
+
if (writePathSelection)
|
|
560
|
+
await refreshRootWritePathSelection(writePathSelection);
|
|
630
561
|
params.assertBeforeMutation?.();
|
|
562
|
+
if (writePathSelection)
|
|
563
|
+
assertRootWritePathSelectionSync(root, writePathSelection);
|
|
631
564
|
handle = await fs.open(ioPath, createFlags, mode);
|
|
632
565
|
createdForWrite = true;
|
|
633
566
|
}
|
|
@@ -648,36 +581,60 @@ async function openWritableFileInRoot(root, params) {
|
|
|
648
581
|
let createdIdentity = null;
|
|
649
582
|
try {
|
|
650
583
|
const stat = fsSync.fstatSync(handle.fd);
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
throw new FsSafeError("not-file", "path is not a regular file under root");
|
|
656
|
-
}
|
|
657
|
-
if (stat.nlink > 1) {
|
|
658
|
-
throw hardlinkedPathNotAllowedError();
|
|
659
|
-
}
|
|
660
|
-
try {
|
|
661
|
-
const lstat = fsSync.lstatSync(ioPath);
|
|
662
|
-
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
663
|
-
throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
|
|
664
|
-
}
|
|
665
|
-
if (!sameFileIdentity(stat, lstat)) {
|
|
666
|
-
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
584
|
+
const identity = inspectFileIdentitySync(() => {
|
|
585
|
+
const observed = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
586
|
+
if (!observed.isFile()) {
|
|
587
|
+
throw new FsSafeError("not-file", "path is not a regular file under root");
|
|
667
588
|
}
|
|
589
|
+
if (observed.nlink > 1n)
|
|
590
|
+
throw hardlinkedPathNotAllowedError();
|
|
591
|
+
return observed;
|
|
592
|
+
});
|
|
593
|
+
if (createdForWrite)
|
|
594
|
+
createdIdentity = identity;
|
|
595
|
+
let observedIoPath = false;
|
|
596
|
+
try {
|
|
597
|
+
inspectFileIdentitySync(() => {
|
|
598
|
+
const lstat = fsSync.lstatSync(ioPath, { bigint: true });
|
|
599
|
+
observedIoPath = true;
|
|
600
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
601
|
+
throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
|
|
602
|
+
}
|
|
603
|
+
if (lstat.nlink > 1n)
|
|
604
|
+
throw hardlinkedPathNotAllowedError();
|
|
605
|
+
return lstat;
|
|
606
|
+
}, identity);
|
|
668
607
|
}
|
|
669
608
|
catch (err) {
|
|
670
|
-
if (
|
|
609
|
+
if (isNotFoundPathError(err) && !observedIoPath) {
|
|
610
|
+
// The opened file may have been renamed before its first pathname
|
|
611
|
+
// observation. The descriptor-bound resolver below must still find it.
|
|
612
|
+
}
|
|
613
|
+
else if (err instanceof FsSafeError && err.code === "path-mismatch") {
|
|
614
|
+
throw new FsSafeError("path-mismatch", "path changed during write", { cause: err });
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
671
617
|
throw err;
|
|
672
618
|
}
|
|
673
619
|
}
|
|
674
|
-
let realPath = await
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
620
|
+
let realPath = (await resolveOpenedFileRealPathForFd(handle.fd, identity, ioPath)).realPath;
|
|
621
|
+
assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
|
|
622
|
+
try {
|
|
623
|
+
inspectFileIdentitySync(() => {
|
|
624
|
+
const realStat = fsSync.statSync(realPath, { bigint: true });
|
|
625
|
+
if (!realStat.isFile()) {
|
|
626
|
+
throw new FsSafeError("not-file", "path is not a regular file under root");
|
|
627
|
+
}
|
|
628
|
+
if (realStat.nlink > 1n)
|
|
629
|
+
throw hardlinkedPathNotAllowedError();
|
|
630
|
+
return realStat;
|
|
631
|
+
}, identity);
|
|
678
632
|
}
|
|
679
|
-
|
|
680
|
-
|
|
633
|
+
catch (err) {
|
|
634
|
+
if (err instanceof FsSafeError && err.code === "path-mismatch") {
|
|
635
|
+
throw new FsSafeError("path-mismatch", "path mismatch", { cause: err });
|
|
636
|
+
}
|
|
637
|
+
throw err;
|
|
681
638
|
}
|
|
682
639
|
const admittedRealPath = admitPathInsideRoot({
|
|
683
640
|
rootPath: root.rootReal,
|
|
@@ -689,14 +646,26 @@ async function openWritableFileInRoot(root, params) {
|
|
|
689
646
|
}
|
|
690
647
|
realPath = admittedRealPath.path;
|
|
691
648
|
realPathForCleanup = realPath;
|
|
649
|
+
assertRootFallbackWritePath(params.expectedWritePath, realPath);
|
|
650
|
+
const writeSelection = writePathSelection
|
|
651
|
+
? createRootWriteSelectionForFd(writePathSelection, handle.fd)
|
|
652
|
+
: undefined;
|
|
692
653
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
693
654
|
// irreversible side effects if a symlink target changes before validation.
|
|
694
655
|
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
|
|
656
|
+
if (writeSelection)
|
|
657
|
+
await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
|
|
695
658
|
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
696
659
|
params.assertBeforeMutation?.();
|
|
660
|
+
if (writeSelection)
|
|
661
|
+
assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
|
|
697
662
|
await handle.truncate(0);
|
|
698
663
|
}
|
|
699
|
-
|
|
664
|
+
if (writeSelection) {
|
|
665
|
+
await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
|
|
666
|
+
assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
|
|
667
|
+
}
|
|
668
|
+
const result = {
|
|
700
669
|
handle,
|
|
701
670
|
containment: "best-effort",
|
|
702
671
|
createdForWrite,
|
|
@@ -704,6 +673,9 @@ async function openWritableFileInRoot(root, params) {
|
|
|
704
673
|
stat,
|
|
705
674
|
[Symbol.asyncDispose]: () => handle.close().catch(() => undefined),
|
|
706
675
|
};
|
|
676
|
+
if (writeSelection)
|
|
677
|
+
retainRootWriteSelection(result, writeSelection);
|
|
678
|
+
return { opened: result, identity };
|
|
707
679
|
}
|
|
708
680
|
catch (err) {
|
|
709
681
|
const cleanupCreatedPath = createdForWrite && err instanceof FsSafeError;
|
|
@@ -718,7 +690,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
718
690
|
async function appendFileInRoot(root, params) {
|
|
719
691
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
720
692
|
try {
|
|
721
|
-
const target = await openWritableFileInRoot(root, {
|
|
693
|
+
const { opened: target, identity } = await openWritableFileInRoot(root, {
|
|
722
694
|
relativePath: params.relativePath,
|
|
723
695
|
mkdir: params.mkdir,
|
|
724
696
|
mode: params.mode,
|
|
@@ -733,7 +705,7 @@ async function appendFileInRoot(root, params) {
|
|
|
733
705
|
const cleanup = __addDisposableResource(env_1, {
|
|
734
706
|
async [Symbol.asyncDispose]() {
|
|
735
707
|
if (!dispatched && target.createdForWrite) {
|
|
736
|
-
await removePathIfIdentityUnchanged(target.realPath,
|
|
708
|
+
await removePathIfIdentityUnchanged(target.realPath, identity);
|
|
737
709
|
}
|
|
738
710
|
},
|
|
739
711
|
}, true);
|
|
@@ -786,14 +758,16 @@ async function appendFileInRoot(root, params) {
|
|
|
786
758
|
}
|
|
787
759
|
async function removePathInRoot(root, params) {
|
|
788
760
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
761
|
+
const removalReceipts = params.recursive ? undefined : new RemovalPathReceipts();
|
|
789
762
|
const resolved = await resolvePinnedPathInRoot(root, {
|
|
790
763
|
relativePath: params.relativePath,
|
|
791
764
|
denyMutations: params.denyMutations,
|
|
792
765
|
mutationSymlinks: params.mutationSymlinks,
|
|
793
766
|
remove: true,
|
|
767
|
+
removalReceipts,
|
|
794
768
|
});
|
|
795
769
|
try {
|
|
796
|
-
await removePathInRootFallback(root, resolved.resolved, params);
|
|
770
|
+
await removePathInRootFallback(root, resolved.resolved, params, removalReceipts);
|
|
797
771
|
}
|
|
798
772
|
catch (error) {
|
|
799
773
|
if (params.recursive)
|
|
@@ -803,9 +777,41 @@ async function removePathInRoot(root, params) {
|
|
|
803
777
|
}
|
|
804
778
|
async function mkdirPathInRoot(root, params) {
|
|
805
779
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
806
|
-
const
|
|
780
|
+
const policy = params.denyMutations === undefined && params.mutationSymlinks === undefined
|
|
781
|
+
? undefined
|
|
782
|
+
: snapshotPinnedMutationPolicy(params.denyMutations, params.mutationSymlinks);
|
|
783
|
+
const resolveCurrent = policy
|
|
784
|
+
? async () => await resolvePinnedPathInRoot(root, {
|
|
785
|
+
...params,
|
|
786
|
+
denyMutations: policy.denyMutations,
|
|
787
|
+
mutationSymlinks: policy.mutationSymlinks,
|
|
788
|
+
})
|
|
789
|
+
: undefined;
|
|
790
|
+
const resolved = resolveCurrent
|
|
791
|
+
? await resolveCurrent()
|
|
792
|
+
: await resolvePinnedPathInRoot(root, params);
|
|
793
|
+
const prepared = policy && resolved.relativePosix !== ""
|
|
794
|
+
? await preparePinnedWriteMutationAdmission({
|
|
795
|
+
rootReal: resolved.rootReal,
|
|
796
|
+
rootWithSep: ensureTrailingSep(resolved.rootReal),
|
|
797
|
+
rootIdentity: root.rootIdentity,
|
|
798
|
+
resolvedTargetPath: resolved.resolved,
|
|
799
|
+
originalPath: params.relativePath,
|
|
800
|
+
defaultRelativeParentPath: path.posix.dirname(resolved.relativePosix) === "."
|
|
801
|
+
? ""
|
|
802
|
+
: path.posix.dirname(resolved.relativePosix),
|
|
803
|
+
policy,
|
|
804
|
+
resolveCurrent: resolveCurrent,
|
|
805
|
+
})
|
|
806
|
+
: undefined;
|
|
807
|
+
if (prepared?.mutationAdmission) {
|
|
808
|
+
await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(resolved.resolved);
|
|
809
|
+
}
|
|
807
810
|
try {
|
|
808
|
-
|
|
811
|
+
if (prepared?.mutationAdmission && params.assertBeforeMutation === undefined &&
|
|
812
|
+
await tryMkdirAtExactParent(root, resolved.resolved, prepared.mutationAdmission))
|
|
813
|
+
return;
|
|
814
|
+
await mkdirPathFallback(root, resolved, params.assertBeforeMutation, policy?.mutationSymlinks !== undefined, prepared?.mutationAdmission);
|
|
809
815
|
}
|
|
810
816
|
catch (error) {
|
|
811
817
|
throw normalizePinnedPathError(error);
|
|
@@ -842,6 +848,7 @@ async function commitPinnedWriteInRoot(root, pinned, params, input) {
|
|
|
842
848
|
input,
|
|
843
849
|
maxBytes: params.maxBytes,
|
|
844
850
|
rootIdentity: root.rootIdentity,
|
|
851
|
+
mutationAdmission: pinned.mutationAdmission,
|
|
845
852
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
846
853
|
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
847
854
|
verifyingPublication = true;
|
|
@@ -883,6 +890,7 @@ async function copyFileInRoot(root, params) {
|
|
|
883
890
|
let sourceIdentity;
|
|
884
891
|
if (typeof params.source === "string") {
|
|
885
892
|
assertNoNulPathInput(params.source, "source path contains a NUL byte");
|
|
893
|
+
assertNoWindowsPathAlias(params.source, "filesystem", "source path uses a Windows filesystem namespace alias");
|
|
886
894
|
({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
|
|
887
895
|
hardlinks: params.sourceHardlinks,
|
|
888
896
|
}));
|
|
@@ -934,6 +942,7 @@ async function copyFileInRoot(root, params) {
|
|
|
934
942
|
onPublished: observer.onPublished,
|
|
935
943
|
input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
|
|
936
944
|
rootIdentity: root.rootIdentity,
|
|
945
|
+
mutationAdmission: pinned.mutationAdmission,
|
|
937
946
|
});
|
|
938
947
|
}
|
|
939
948
|
catch (error) {
|
|
@@ -953,66 +962,6 @@ async function copyFileInRoot(root, params) {
|
|
|
953
962
|
await source.handle.close().catch(() => { });
|
|
954
963
|
}
|
|
955
964
|
}
|
|
956
|
-
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
957
|
-
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
958
|
-
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
959
|
-
const current = fsSync.lstatSync(targetPath);
|
|
960
|
-
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
|
|
961
|
-
return;
|
|
962
|
-
await fs.unlink(targetPath);
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
|
|
966
|
-
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
967
|
-
relativePath,
|
|
968
|
-
denyMutations,
|
|
969
|
-
mutationSymlinks,
|
|
970
|
-
});
|
|
971
|
-
// resolvePathInRoot already admits and rebases the path at the exact Root
|
|
972
|
-
// boundary, so any actual escape is rejected upstream.
|
|
973
|
-
const relativeResolved = path.relative(rootReal, resolved);
|
|
974
|
-
if (path.isAbsolute(relativeResolved)) {
|
|
975
|
-
throw outsideWorkspaceError();
|
|
976
|
-
}
|
|
977
|
-
const relativePosix = relativeResolved
|
|
978
|
-
? relativeResolved.split(path.sep).join(path.posix.sep)
|
|
979
|
-
: "";
|
|
980
|
-
const basename = path.posix.basename(relativePosix);
|
|
981
|
-
if (!basename || basename === "." || basename === "/") {
|
|
982
|
-
throw new FsSafeError("invalid-path", "invalid target path");
|
|
983
|
-
}
|
|
984
|
-
// Exclusive publication cannot inherit an existing file's mode. Keep type
|
|
985
|
-
// and alias checks, but never open a competing owner's record just to reject it.
|
|
986
|
-
if (!overwrite) {
|
|
987
|
-
try {
|
|
988
|
-
const existing = fsSync.statSync(resolved);
|
|
989
|
-
if (!existing.isFile())
|
|
990
|
-
throw new FsSafeError("not-file", "not a file");
|
|
991
|
-
if (existing.nlink > 1)
|
|
992
|
-
throw hardlinkedPathNotAllowedError();
|
|
993
|
-
throw new FsSafeError("already-exists", "file already exists");
|
|
994
|
-
}
|
|
995
|
-
catch (error) {
|
|
996
|
-
if (!isNotFoundPathError(error))
|
|
997
|
-
throw error;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
const mode = overwrite
|
|
1001
|
-
? await inheritWriteTargetMode({
|
|
1002
|
-
targetPath: resolved,
|
|
1003
|
-
rootWithSep,
|
|
1004
|
-
rootIdentity: root.rootIdentity,
|
|
1005
|
-
requestedMode,
|
|
1006
|
-
})
|
|
1007
|
-
: requestedMode ?? 0o600;
|
|
1008
|
-
return {
|
|
1009
|
-
rootReal,
|
|
1010
|
-
targetPath: resolved,
|
|
1011
|
-
relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
|
|
1012
|
-
basename,
|
|
1013
|
-
mode,
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
965
|
async function resolvePinnedPathInRoot(root, params) {
|
|
1017
966
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
1018
967
|
allowRoot: params.allowRoot,
|
|
@@ -1021,6 +970,7 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
1021
970
|
protectDenyMutationAncestors: params.remove === true,
|
|
1022
971
|
relativePath: params.relativePath,
|
|
1023
972
|
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
973
|
+
removalReceipts: params.removalReceipts,
|
|
1024
974
|
});
|
|
1025
975
|
}
|
|
1026
976
|
async function resolvePinnedOperationPathInRoot(root, params) {
|
|
@@ -1028,6 +978,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
1028
978
|
relativePath: params.relativePath,
|
|
1029
979
|
policy: params.policy,
|
|
1030
980
|
mutationSymlinks: params.mutationSymlinks,
|
|
981
|
+
removalReceipts: params.removalReceipts,
|
|
1031
982
|
});
|
|
1032
983
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
1033
984
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
@@ -1057,12 +1008,12 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
1057
1008
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
1058
1009
|
}
|
|
1059
1010
|
async function resolvePinnedRootPathInRoot(root, params) {
|
|
1060
|
-
await assertRootIdentityCurrent(root);
|
|
1011
|
+
await assertRootIdentityCurrent(root, params.removalReceipts?.observeRoot);
|
|
1061
1012
|
const rootReal = root.rootReal;
|
|
1062
1013
|
let resolved;
|
|
1063
1014
|
try {
|
|
1064
1015
|
const expandedPath = await expandRelativePathWithHome(params.relativePath);
|
|
1065
|
-
|
|
1016
|
+
const resolution = {
|
|
1066
1017
|
absolutePath: path.isAbsolute(expandedPath)
|
|
1067
1018
|
? expandedPath
|
|
1068
1019
|
: `${ensureTrailingSep(rootReal)}${expandedPath}`,
|
|
@@ -1072,7 +1023,8 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
1072
1023
|
boundaryLabel: "root",
|
|
1073
1024
|
policy: params.policy,
|
|
1074
1025
|
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1075
|
-
}
|
|
1026
|
+
};
|
|
1027
|
+
resolved = await (params.removalReceipts ? resolveRootPathForRemoval(resolution, params.removalReceipts) : resolveRootPath(resolution));
|
|
1076
1028
|
}
|
|
1077
1029
|
catch (err) {
|
|
1078
1030
|
if (err instanceof FsSafeError && err.code === "symlink")
|
|
@@ -1086,31 +1038,20 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
1086
1038
|
canonicalPath: resolved.canonicalPath,
|
|
1087
1039
|
};
|
|
1088
1040
|
}
|
|
1089
|
-
async function mkdirPathFallback(root, resolved, assertBeforeMutation, rejectSymlinks = false) {
|
|
1090
|
-
await mkdirPathComponentsWithGuards({
|
|
1091
|
-
rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
|
|
1092
|
-
rootIdentity: root.rootIdentity,
|
|
1093
|
-
rejectSymlinks,
|
|
1094
|
-
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
1041
|
async function statPathFallback(root, relativePath) {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
await
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
if (isNotFoundPathError(error)) {
|
|
1106
|
-
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
1107
|
-
}
|
|
1108
|
-
throw error;
|
|
1109
|
-
}
|
|
1042
|
+
const initialObservationHook = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
|
|
1043
|
+
const observed = initialObservationHook
|
|
1044
|
+
? undefined
|
|
1045
|
+
: await resolvePinnedObservedPathInRoot(root, relativePath, "stat");
|
|
1046
|
+
const resolved = observed ??
|
|
1047
|
+
await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
1048
|
+
return await statResolvedPathInRoot(root, resolved.resolved, resolved.receipt);
|
|
1110
1049
|
}
|
|
1111
1050
|
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1051
|
+
const observed = await resolvePinnedObservedPathInRoot(root, relativePath, "directory");
|
|
1052
|
+
const resolved = observed ??
|
|
1053
|
+
await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
1054
|
+
return await listDirectoryPath(root, resolved.resolved, withFileTypes, resolved.receipt);
|
|
1114
1055
|
}
|
|
1115
1056
|
async function movePathFallback(root, params) {
|
|
1116
1057
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
@@ -1119,11 +1060,12 @@ async function movePathFallback(root, params) {
|
|
|
1119
1060
|
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1120
1061
|
});
|
|
1121
1062
|
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1122
|
-
await resolvePinnedRootPathInRoot(root, {
|
|
1063
|
+
const pinnedSource = await resolvePinnedRootPathInRoot(root, {
|
|
1123
1064
|
relativePath: params.fromRelative,
|
|
1124
1065
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1125
1066
|
mutationSymlinks: params.mutationSymlinks,
|
|
1126
1067
|
});
|
|
1068
|
+
let pinnedTarget;
|
|
1127
1069
|
const target = await resolveGuardedWritePathInRoot(root, {
|
|
1128
1070
|
relativePath: params.toRelative,
|
|
1129
1071
|
denyMutations: params.denyMutations,
|
|
@@ -1131,10 +1073,12 @@ async function movePathFallback(root, params) {
|
|
|
1131
1073
|
allowFinalSymlink: true,
|
|
1132
1074
|
protectDeniedAncestors: true,
|
|
1133
1075
|
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
1134
|
-
await resolvePinnedRootPathInRoot(root, {
|
|
1076
|
+
pinnedTarget = await resolvePinnedRootPathInRoot(root, {
|
|
1135
1077
|
relativePath: params.toRelative,
|
|
1136
1078
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
1137
1079
|
});
|
|
1080
|
+
if (!params.overwrite)
|
|
1081
|
+
return true;
|
|
1138
1082
|
let targetStat;
|
|
1139
1083
|
try {
|
|
1140
1084
|
targetStat = fsSync.lstatSync(resolvedTarget.resolved);
|
|
@@ -1165,18 +1109,16 @@ async function movePathFallback(root, params) {
|
|
|
1165
1109
|
throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
1166
1110
|
}
|
|
1167
1111
|
if (!params.overwrite) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
throw new FsSafeError("already-exists", "destination exists");
|
|
1171
|
-
}
|
|
1172
|
-
catch (error) {
|
|
1173
|
-
if (error instanceof FsSafeError) {
|
|
1174
|
-
throw error;
|
|
1175
|
-
}
|
|
1176
|
-
if (!isNotFoundPathError(error)) {
|
|
1177
|
-
throw error;
|
|
1178
|
-
}
|
|
1112
|
+
if (!pinnedTarget) {
|
|
1113
|
+
throw new FsSafeError("path-mismatch", "destination admission was not completed");
|
|
1179
1114
|
}
|
|
1115
|
+
await movePathNoReplaceNative(root, params, {
|
|
1116
|
+
sourcePath: source.resolved,
|
|
1117
|
+
sourceParentPath: path.dirname(pinnedSource.canonicalPath),
|
|
1118
|
+
targetPath: target.resolved,
|
|
1119
|
+
targetParentPath: path.dirname(pinnedTarget.canonicalPath),
|
|
1120
|
+
});
|
|
1121
|
+
return;
|
|
1180
1122
|
}
|
|
1181
1123
|
const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
|
|
1182
1124
|
const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
|
|
@@ -1209,11 +1151,22 @@ async function movePathFallback(root, params) {
|
|
|
1209
1151
|
}
|
|
1210
1152
|
}
|
|
1211
1153
|
async function writeFileFallback(root, params) {
|
|
1154
|
+
if (params.renameIdentity !== "verify-content-with-lock")
|
|
1155
|
+
return await writeFileFallbackUnlocked(root, params);
|
|
1156
|
+
const policy = snapshotPinnedMutationPolicy(params.denyMutations, params.mutationSymlinks);
|
|
1157
|
+
if (policy)
|
|
1158
|
+
params = { ...params, ...policy };
|
|
1159
|
+
const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, params);
|
|
1160
|
+
await withRootFallbackCompatibilityLock({
|
|
1161
|
+
rootPath: rootReal, rootIdentity: root.rootIdentity, targetPath: resolved, assertBeforeMutation: params.assertBeforeMutation,
|
|
1162
|
+
}, async ({ targetPath, ...binding }) => await writeFileFallbackUnlocked(root, { ...params, ...binding }, targetPath));
|
|
1163
|
+
}
|
|
1164
|
+
async function writeFileFallbackUnlocked(root, params, expectedWritePath) {
|
|
1212
1165
|
if (params.overwrite === false) {
|
|
1213
|
-
await writeMissingFileFallback(root, params);
|
|
1166
|
+
await writeMissingFileFallback(root, params, expectedWritePath);
|
|
1214
1167
|
return;
|
|
1215
1168
|
}
|
|
1216
|
-
const target = await openWritableFileInRoot(root, {
|
|
1169
|
+
const { opened: target, identity: targetIdentity } = await openWritableFileInRoot(root, {
|
|
1217
1170
|
relativePath: params.relativePath,
|
|
1218
1171
|
mkdir: params.mkdir,
|
|
1219
1172
|
// Private, writable placeholder: Windows cannot rename over a read-only file.
|
|
@@ -1222,13 +1175,21 @@ async function writeFileFallback(root, params) {
|
|
|
1222
1175
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
1223
1176
|
mutationSymlinks: params.mutationSymlinks,
|
|
1224
1177
|
truncateExisting: false,
|
|
1178
|
+
expectedWritePath,
|
|
1225
1179
|
});
|
|
1226
|
-
const
|
|
1180
|
+
const policyEnabled = params.denyMutations !== undefined || params.mutationSymlinks !== undefined;
|
|
1181
|
+
const retainedSelection = policyEnabled ? takeRootWriteSelection(target) : undefined;
|
|
1182
|
+
const destinationPath = retainedSelection?.selectedPath ?? target.realPath;
|
|
1227
1183
|
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
1228
|
-
|
|
1184
|
+
if (policyEnabled && !retainedSelection) {
|
|
1229
1185
|
await target.handle.close().catch(() => undefined);
|
|
1230
|
-
throw
|
|
1231
|
-
}
|
|
1186
|
+
throw new FsSafeError("path-mismatch", "write admission state was not retained");
|
|
1187
|
+
}
|
|
1188
|
+
const destinationGuard = retainedSelection?.parentGuard ??
|
|
1189
|
+
await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
|
|
1190
|
+
await target.handle.close().catch(() => undefined);
|
|
1191
|
+
throw error;
|
|
1192
|
+
});
|
|
1232
1193
|
let tempPath = null;
|
|
1233
1194
|
let unregisterTempPath = null;
|
|
1234
1195
|
let writtenHandle;
|
|
@@ -1237,9 +1198,15 @@ async function writeFileFallback(root, params) {
|
|
|
1237
1198
|
let published = false;
|
|
1238
1199
|
try {
|
|
1239
1200
|
if (target.createdForWrite)
|
|
1240
|
-
placeholderIdentity =
|
|
1201
|
+
placeholderIdentity = targetIdentity;
|
|
1241
1202
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1203
|
+
if (retainedSelection) {
|
|
1204
|
+
await refreshRetainedRootWriteAdmission(root, retainedSelection, true, target.handle.fd);
|
|
1205
|
+
}
|
|
1242
1206
|
params.assertBeforeMutation?.();
|
|
1207
|
+
if (retainedSelection) {
|
|
1208
|
+
assertRootWriteSelectionSync(root, retainedSelection, true, target.handle.fd);
|
|
1209
|
+
}
|
|
1243
1210
|
writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
|
|
1244
1211
|
writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
|
|
1245
1212
|
unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
|
|
@@ -1263,41 +1230,27 @@ async function writeFileFallback(root, params) {
|
|
|
1263
1230
|
}
|
|
1264
1231
|
// Windows cannot replace a destination while its old handle remains open.
|
|
1265
1232
|
await target.handle.close();
|
|
1233
|
+
if (retainedSelection) {
|
|
1234
|
+
await refreshRetainedRootWriteAdmission(root, retainedSelection, !target.createdForWrite);
|
|
1235
|
+
}
|
|
1266
1236
|
assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
|
|
1267
1237
|
params.assertBeforeMutation?.();
|
|
1238
|
+
if (retainedSelection) {
|
|
1239
|
+
assertRootWriteSelectionSync(root, retainedSelection, !target.createdForWrite);
|
|
1240
|
+
}
|
|
1268
1241
|
await fs.rename(commitTempPath, destinationPath);
|
|
1269
1242
|
tempPath = null;
|
|
1270
1243
|
published = true;
|
|
1271
1244
|
});
|
|
1272
1245
|
unregisterTempPath();
|
|
1273
1246
|
unregisterTempPath = null;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
});
|
|
1282
|
-
throw error;
|
|
1283
|
-
}
|
|
1284
|
-
if (params.durable !== false)
|
|
1285
|
-
await writtenHandle.sync();
|
|
1286
|
-
try {
|
|
1287
|
-
await verifyAtomicWriteResult({
|
|
1288
|
-
root,
|
|
1289
|
-
targetPath: destinationPath,
|
|
1290
|
-
expectedIdentity: writtenIdentity,
|
|
1291
|
-
fd: writtenHandle.fd,
|
|
1292
|
-
parentGuard: destinationGuard,
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
catch (err) {
|
|
1296
|
-
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
1297
|
-
throw err;
|
|
1298
|
-
}
|
|
1299
|
-
if (params.durable !== false)
|
|
1300
|
-
await syncDirectoryBestEffort(path.dirname(destinationPath));
|
|
1247
|
+
await finishRootFallbackWrite({
|
|
1248
|
+
root, targetPath: destinationPath, handle: writtenHandle, identity: writtenIdentity,
|
|
1249
|
+
parentGuard: destinationGuard, mode, options: params,
|
|
1250
|
+
// Read/write access is needed to sync the accepted destination on Windows.
|
|
1251
|
+
openForCompatibility: () => openVerifiedLocalFile(destinationPath, { hardlinks: "reject", readWrite: true }),
|
|
1252
|
+
onVerificationFailure: err => emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`),
|
|
1253
|
+
});
|
|
1301
1254
|
}
|
|
1302
1255
|
finally {
|
|
1303
1256
|
if (!published && target.createdForWrite) {
|
|
@@ -1315,16 +1268,31 @@ async function writeFileFallback(root, params) {
|
|
|
1315
1268
|
unregisterTempPath?.();
|
|
1316
1269
|
}
|
|
1317
1270
|
}
|
|
1318
|
-
async function writeMissingFileFallback(root, params) {
|
|
1319
|
-
const
|
|
1271
|
+
async function writeMissingFileFallback(root, params, expectedWritePath) {
|
|
1272
|
+
const guardedTarget = params.denyMutations === undefined && params.mutationSymlinks === undefined
|
|
1273
|
+
? undefined
|
|
1274
|
+
: await resolveGuardedWriteTargetInRoot(root, {
|
|
1275
|
+
relativePath: params.relativePath,
|
|
1276
|
+
denyMutations: params.denyMutations,
|
|
1277
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1278
|
+
});
|
|
1279
|
+
const { resolved } = guardedTarget?.resolvedPath ?? await resolveGuardedWritePathInRoot(root, {
|
|
1320
1280
|
relativePath: params.relativePath,
|
|
1321
|
-
denyMutations: params.denyMutations,
|
|
1322
|
-
mutationSymlinks: params.mutationSymlinks,
|
|
1323
1281
|
});
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
:
|
|
1327
|
-
|
|
1282
|
+
const prepared = guardedTarget ? await prepareSharedRootWriteTarget(root, {
|
|
1283
|
+
relativePath: params.relativePath, guardedTarget, mkdir: params.mkdir,
|
|
1284
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
1285
|
+
}) : undefined;
|
|
1286
|
+
const mutationAdmission = prepared?.mutationAdmission;
|
|
1287
|
+
const preparedParent = prepared?.preparedParent;
|
|
1288
|
+
const targetPath = prepared?.targetPath ?? (params.mkdir === false ? resolved :
|
|
1289
|
+
await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation));
|
|
1290
|
+
assertRootFallbackWritePath(expectedWritePath, targetPath);
|
|
1291
|
+
const pathSelection = guardedTarget
|
|
1292
|
+
? await prepareGuardedRootWritePathSelection(guardedTarget, targetPath, targetPath, preparedParent)
|
|
1293
|
+
: undefined;
|
|
1294
|
+
const parentGuard = pathSelection?.parentGuard ??
|
|
1295
|
+
await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
|
|
1328
1296
|
let created = false;
|
|
1329
1297
|
let createdIdentity;
|
|
1330
1298
|
let writtenHandle;
|
|
@@ -1333,6 +1301,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1333
1301
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1334
1302
|
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1335
1303
|
params.assertBeforeMutation?.();
|
|
1304
|
+
if (mutationAdmission)
|
|
1305
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
1336
1306
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
|
|
1337
1307
|
writtenHandle = handle;
|
|
1338
1308
|
created = true;
|
|
@@ -1343,6 +1313,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1343
1313
|
assertBeforeMutation: () => {
|
|
1344
1314
|
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1345
1315
|
params.assertBeforeMutation?.();
|
|
1316
|
+
if (mutationAdmission)
|
|
1317
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
1346
1318
|
},
|
|
1347
1319
|
});
|
|
1348
1320
|
if (params.durable !== false)
|