@openclaw/fs-safe 0.11.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 +119 -0
- package/README.md +15 -8
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +3 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +3 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -3
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +13 -9
- 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 +55 -46
- 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 +50 -17
- 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/bounded-read.js +2 -2
- 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/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +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 +12 -2
- 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-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +9 -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 +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +85 -23
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +10 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +217 -33
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +16 -5
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +67 -19
- package/dist/json-durable-queue-ownership.d.ts +8 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +134 -43
- 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 +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +61 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +83 -134
- 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 +21 -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 +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +37 -0
- 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 +35 -6
- 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 +152 -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 +29 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +182 -0
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +89 -16
- 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 +182 -33
- 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 +316 -325
- 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-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-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 +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +63 -22
- 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 +13 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +263 -73
- 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 -29
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +89 -178
- 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-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- 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-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +20 -7
- 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 +18 -7
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +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-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +98 -10
- 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 -1
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +181 -50
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -12
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +45 -15
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- 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 +133 -4
- 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 +20 -16
- 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 +28 -1
- package/docs/archive.md +29 -2
- package/docs/atomic.md +27 -3
- package/docs/contributing.md +4 -1
- package/docs/copy.md +4 -1
- package/docs/durability.md +18 -2
- package/docs/errors.md +14 -6
- package/docs/file-store.md +30 -3
- package/docs/filename.md +21 -7
- package/docs/guest.md +5 -0
- package/docs/install-path.md +61 -15
- package/docs/install.md +12 -8
- 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 +28 -9
- package/docs/native.md +46 -10
- 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 +12 -1
- package/docs/permissions.md +26 -1
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +14 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +21 -4
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +21 -16
- package/docs/security-model.md +76 -9
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +27 -2
- package/docs/temp.md +262 -34
- 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,32 @@ 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";
|
|
79
|
-
import {
|
|
78
|
+
import { resolveRootPath, resolveRootPathForRemoval } from "./root-path.js";
|
|
79
|
+
import { RemovalPathReceipts } from "./root-remove-receipt.js";
|
|
80
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
81
|
+
import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-list.js";
|
|
82
|
+
import { statResolvedPathInRoot } from "./root-path-stat.js";
|
|
80
83
|
import { entriesInRoot } from "./root-entries.js";
|
|
84
|
+
import { assertMoveMutationAllowed } from "./root-move-preflight.js";
|
|
81
85
|
import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
82
86
|
import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
|
|
83
87
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
@@ -87,12 +91,19 @@ import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
|
87
91
|
import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
|
|
88
92
|
import { serializePathWrite } from "./write-queue.js";
|
|
89
93
|
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
90
|
-
import {
|
|
91
|
-
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";
|
|
92
101
|
import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
|
|
93
102
|
import { writeAllToFile } from "./write-file-handle.js";
|
|
94
103
|
import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
|
|
95
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";
|
|
96
107
|
import { mergeReadOptions, readDefaults, } from "./root-options.js";
|
|
97
108
|
import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
|
|
98
109
|
export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
|
|
@@ -103,8 +114,6 @@ function logWarn(message) {
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
106
|
-
const OPEN_READ_FLAGS = resolveReadOpenFlags();
|
|
107
|
-
const OPEN_READ_FOLLOW_FLAGS = resolveReadOpenFlags({ followSymlinks: true });
|
|
108
117
|
const OPEN_WRITE_EXISTING_FLAGS = fsConstants.O_WRONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0) |
|
|
109
118
|
resolveNonblockingWriteFlag();
|
|
110
119
|
const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
|
|
@@ -128,69 +137,9 @@ function openResult(params) {
|
|
|
128
137
|
};
|
|
129
138
|
}
|
|
130
139
|
async function openVerifiedLocalFile(filePath, options) {
|
|
131
|
-
assertNoUnsafeDeviceReadPath(filePath);
|
|
132
140
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
133
|
-
|
|
134
|
-
let observedBeforeOpen = false;
|
|
135
|
-
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
136
|
-
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
141
|
+
const { handle, stat, identity, preOpenStat } = await openLocalFileDescriptor(filePath, options);
|
|
137
142
|
try {
|
|
138
|
-
preOpenStat = await inspectFileIdentity(async () => {
|
|
139
|
-
const stat = fsSync.lstatSync(filePath, { bigint: true });
|
|
140
|
-
observedBeforeOpen = true;
|
|
141
|
-
if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
142
|
-
throw new FsSafeError("symlink", "symlink not allowed");
|
|
143
|
-
}
|
|
144
|
-
if (!stat.isFile() && !stat.isSymbolicLink()) {
|
|
145
|
-
throw new FsSafeError("not-file", "not a file");
|
|
146
|
-
}
|
|
147
|
-
return stat;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
if (err instanceof FsSafeError || observedBeforeOpen) {
|
|
152
|
-
throw err;
|
|
153
|
-
}
|
|
154
|
-
// Only an initial lookup failure falls through; a failed re-inspection aborts.
|
|
155
|
-
}
|
|
156
|
-
if (preOpenStat) {
|
|
157
|
-
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
158
|
-
}
|
|
159
|
-
const openFlags = options?.symlinks === "follow-within-root"
|
|
160
|
-
? OPEN_READ_FOLLOW_FLAGS
|
|
161
|
-
: OPEN_READ_FLAGS;
|
|
162
|
-
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
163
|
-
let handle;
|
|
164
|
-
try {
|
|
165
|
-
handle = await fs.open(filePath, openFlags).catch((error) => recordFileOpenFailure(isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
if (isSymlinkOpenError(err)) {
|
|
169
|
-
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
170
|
-
}
|
|
171
|
-
// Defensive: if open still throws EISDIR (e.g. race), sanitize so it never leaks.
|
|
172
|
-
if (hasNodeErrorCode(err, "EISDIR")) {
|
|
173
|
-
throw new FsSafeError("not-file", "not a file");
|
|
174
|
-
}
|
|
175
|
-
throw err;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
179
|
-
const stat = fsSync.fstatSync(handle.fd);
|
|
180
|
-
if (!stat.isFile()) {
|
|
181
|
-
throw new FsSafeError("not-file", "not a file");
|
|
182
|
-
}
|
|
183
|
-
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
184
|
-
let openedIdentity;
|
|
185
|
-
const identity = await inspectFileIdentity(async () => (openedIdentity = fsSync.fstatSync(handle.fd, { bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
|
|
186
|
-
if ([0, 1].includes(stat.nlink)) {
|
|
187
|
-
await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
|
|
188
|
-
}
|
|
189
|
-
throw error;
|
|
190
|
-
});
|
|
191
|
-
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
192
|
-
throw hardlinkedPathNotAllowedError();
|
|
193
|
-
}
|
|
194
143
|
const inspectPathIdentity = async (inspect) => {
|
|
195
144
|
try {
|
|
196
145
|
return await inspectFileIdentity(inspect, identity);
|
|
@@ -203,15 +152,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
203
152
|
throw failure;
|
|
204
153
|
}
|
|
205
154
|
};
|
|
206
|
-
await inspectPathIdentity(async () =>
|
|
207
|
-
const pathStat = options?.symlinks === "follow-within-root"
|
|
208
|
-
? fsSync.statSync(filePath, { bigint: true })
|
|
209
|
-
: fsSync.lstatSync(filePath, { bigint: true });
|
|
210
|
-
if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
211
|
-
throw new FsSafeError("symlink", "symlink not allowed");
|
|
212
|
-
}
|
|
213
|
-
return pathStat;
|
|
214
|
-
});
|
|
155
|
+
await inspectPathIdentity(async () => inspectOpenedPathIdentitySync(filePath, options?.symlinks));
|
|
215
156
|
await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
|
|
216
157
|
const resolved = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
217
158
|
.catch(async (error) => {
|
|
@@ -221,6 +162,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
221
162
|
throw error;
|
|
222
163
|
});
|
|
223
164
|
const { realPath } = resolved;
|
|
165
|
+
assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
|
|
224
166
|
let resolvedStat = resolved.stat;
|
|
225
167
|
await inspectPathIdentity(async () => {
|
|
226
168
|
// Reuse the post-realpath observation; unknown Windows identities still
|
|
@@ -240,12 +182,14 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
240
182
|
}
|
|
241
183
|
}
|
|
242
184
|
export class RootHandle {
|
|
185
|
+
rootGuard;
|
|
243
186
|
rootIdentity;
|
|
244
187
|
rootDir;
|
|
245
188
|
rootReal;
|
|
246
189
|
rootWithSep;
|
|
247
190
|
defaults;
|
|
248
191
|
constructor(context, defaults = {}) {
|
|
192
|
+
this.rootGuard = context.rootGuard;
|
|
249
193
|
this.rootIdentity = context.rootIdentity;
|
|
250
194
|
this.rootDir = context.rootDir;
|
|
251
195
|
this.rootReal = context.rootReal;
|
|
@@ -255,6 +199,7 @@ export class RootHandle {
|
|
|
255
199
|
get context() {
|
|
256
200
|
return {
|
|
257
201
|
rootDir: this.rootDir,
|
|
202
|
+
rootGuard: this.rootGuard,
|
|
258
203
|
rootIdentity: this.rootIdentity,
|
|
259
204
|
rootReal: this.rootReal,
|
|
260
205
|
rootWithSep: this.rootWithSep,
|
|
@@ -309,7 +254,7 @@ export class RootHandle {
|
|
|
309
254
|
async openWritable(relativePath, options = {}) {
|
|
310
255
|
assertValidRootDestinationPath(relativePath);
|
|
311
256
|
const writeMode = options.writeMode ?? "replace";
|
|
312
|
-
|
|
257
|
+
const target = await openWritableFileInRoot(this.context, {
|
|
313
258
|
relativePath,
|
|
314
259
|
mkdir: this.defaults.mkdir,
|
|
315
260
|
mode: this.defaults.mode,
|
|
@@ -317,6 +262,7 @@ export class RootHandle {
|
|
|
317
262
|
append: writeMode === "append",
|
|
318
263
|
truncateExisting: writeMode === "replace",
|
|
319
264
|
}).catch(rethrowMutationAuthorityError);
|
|
265
|
+
return target.opened;
|
|
320
266
|
}
|
|
321
267
|
async append(relativePath, data, options = {}) {
|
|
322
268
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -428,6 +374,7 @@ export class RootHandle {
|
|
|
428
374
|
assertValidRootDestinationPath(toRelative);
|
|
429
375
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
430
376
|
const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
|
|
377
|
+
const overwrite = options.overwrite ?? false;
|
|
431
378
|
await assertMoveMutationAllowed(this.context, {
|
|
432
379
|
fromRelative,
|
|
433
380
|
toRelative,
|
|
@@ -438,7 +385,7 @@ export class RootHandle {
|
|
|
438
385
|
denyMutations,
|
|
439
386
|
assertBeforeMutation,
|
|
440
387
|
mutationSymlinks,
|
|
441
|
-
overwrite
|
|
388
|
+
overwrite,
|
|
442
389
|
toRelative,
|
|
443
390
|
}).catch(rethrowMutationAuthorityError);
|
|
444
391
|
}
|
|
@@ -471,31 +418,36 @@ export async function root(rootDir, defaults = {}) {
|
|
|
471
418
|
export function rootFromDirectoryGuard(guard, defaults = {}) {
|
|
472
419
|
normalizeMaxBytes(defaults.maxBytes);
|
|
473
420
|
return new RootHandle({
|
|
474
|
-
rootDir:
|
|
421
|
+
rootDir: resolvePathPreservingWindowsRoot(guard.dir),
|
|
422
|
+
rootGuard: { dir: guard.realPath, realPath: guard.realPath, stat: guard.stat },
|
|
475
423
|
rootIdentity: { dev: guard.stat.dev, ino: guard.stat.ino },
|
|
476
424
|
rootReal: guard.realPath,
|
|
477
425
|
rootWithSep: ensureTrailingSep(guard.realPath),
|
|
478
426
|
}, defaults);
|
|
479
427
|
}
|
|
480
428
|
async function openFileInRoot(root, params) {
|
|
481
|
-
const {
|
|
429
|
+
const { resolved } = await resolvePathInRoot(root, params.relativePath, {
|
|
482
430
|
allowFinalSymlink: true,
|
|
483
431
|
rejectUnsafeDeviceReads: true,
|
|
484
432
|
...readSymlinkResolution(params.symlinks),
|
|
485
433
|
resolveCanonical: true,
|
|
486
434
|
});
|
|
487
|
-
const
|
|
435
|
+
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
436
|
+
if (fsSafeTestHooks?.afterRootReadPathResolution) {
|
|
437
|
+
await fsSafeTestHooks.afterRootReadPathResolution(resolved);
|
|
438
|
+
}
|
|
439
|
+
const { handle, stat, identity } = await openLocalFileDescriptor(resolved, {
|
|
488
440
|
symlinks: params.symlinks,
|
|
489
441
|
});
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
return
|
|
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,
|
|
449
|
+
});
|
|
450
|
+
return openResult(admitted);
|
|
499
451
|
}
|
|
500
452
|
async function readFileInRoot(root, params) {
|
|
501
453
|
const opened = await openFileInRoot(root, params);
|
|
@@ -526,8 +478,10 @@ export async function readLocalFileSafely(params) {
|
|
|
526
478
|
}
|
|
527
479
|
}
|
|
528
480
|
export async function openLocalFileSafely(params) {
|
|
529
|
-
|
|
530
|
-
|
|
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;
|
|
531
485
|
}
|
|
532
486
|
function emitWriteBoundaryWarning(reason) {
|
|
533
487
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
@@ -538,56 +492,38 @@ function buildAtomicWriteTempPath(targetPath) {
|
|
|
538
492
|
function rootWriteQueueKey(root, relativePath) {
|
|
539
493
|
return `${root.rootReal}\0${relativePath}`;
|
|
540
494
|
}
|
|
541
|
-
async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
|
|
542
|
-
const parentPath = await mkdirPathComponentsWithGuards({
|
|
543
|
-
rootReal,
|
|
544
|
-
targetPath: path.dirname(targetPath),
|
|
545
|
-
assertBeforeMutation,
|
|
546
|
-
});
|
|
547
|
-
// Continue through the guarded walk's real parent instead of re-entering
|
|
548
|
-
// the original path through a symlinked component.
|
|
549
|
-
return path.join(parentPath, path.basename(targetPath));
|
|
550
|
-
}
|
|
551
|
-
async function resolveGuardedWritePathInRoot(root, params) {
|
|
552
|
-
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
553
|
-
aliasErrorCode: "path-alias",
|
|
554
|
-
rejectAmbiguousParents: true,
|
|
555
|
-
allowFinalSymlink: params.allowFinalSymlink,
|
|
556
|
-
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
557
|
-
});
|
|
558
|
-
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
559
|
-
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
560
|
-
try {
|
|
561
|
-
await assertNoPathAliasEscape({
|
|
562
|
-
absolutePath: resolvedPath.resolved,
|
|
563
|
-
rootPath: resolvedPath.rootReal,
|
|
564
|
-
boundaryLabel: "root",
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
throw new FsSafeError("path-alias", "path alias escape blocked", {
|
|
569
|
-
cause: error instanceof Error ? error : undefined,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return resolvedPath;
|
|
574
|
-
}
|
|
575
495
|
async function openWritableFileInRoot(root, params) {
|
|
576
|
-
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, {
|
|
577
504
|
relativePath: params.relativePath,
|
|
578
|
-
denyMutations: params.denyMutations,
|
|
579
|
-
mutationSymlinks: params.mutationSymlinks,
|
|
580
505
|
});
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
:
|
|
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;
|
|
584
515
|
try {
|
|
585
516
|
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
586
517
|
const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
|
|
587
|
-
|
|
518
|
+
const admittedRealPath = admitPathInsideRoot({
|
|
519
|
+
rootPath: root.rootReal,
|
|
520
|
+
candidatePath: resolvedRealPath,
|
|
521
|
+
rootIdentity: root.rootIdentity,
|
|
522
|
+
});
|
|
523
|
+
if (!admittedRealPath) {
|
|
588
524
|
throw outsideWorkspaceError();
|
|
589
525
|
}
|
|
590
|
-
ioPath =
|
|
526
|
+
ioPath = admittedRealPath.path;
|
|
591
527
|
}
|
|
592
528
|
catch (err) {
|
|
593
529
|
if (err instanceof FsSafeError) {
|
|
@@ -597,13 +533,20 @@ async function openWritableFileInRoot(root, params) {
|
|
|
597
533
|
throw err;
|
|
598
534
|
}
|
|
599
535
|
}
|
|
536
|
+
assertRootFallbackWritePath(params.expectedWritePath, ioPath);
|
|
600
537
|
const mode = params.mode ?? 0o600;
|
|
601
538
|
let handle;
|
|
602
539
|
let createdForWrite = false;
|
|
540
|
+
let writePathSelection = undefined;
|
|
603
541
|
const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
|
|
604
542
|
const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
|
|
605
543
|
try {
|
|
544
|
+
writePathSelection = guardedTarget
|
|
545
|
+
? await prepareGuardedRootWritePathSelection(guardedTarget, ioPath, operationTargetPath, preparedParent)
|
|
546
|
+
: undefined;
|
|
606
547
|
try {
|
|
548
|
+
if (writePathSelection)
|
|
549
|
+
assertRootWritePathSelectionSync(root, writePathSelection);
|
|
607
550
|
handle = await fs.open(ioPath, existingFlags, mode);
|
|
608
551
|
}
|
|
609
552
|
catch (err) {
|
|
@@ -613,7 +556,11 @@ async function openWritableFileInRoot(root, params) {
|
|
|
613
556
|
if (!isNotFoundPathError(err)) {
|
|
614
557
|
throw err;
|
|
615
558
|
}
|
|
559
|
+
if (writePathSelection)
|
|
560
|
+
await refreshRootWritePathSelection(writePathSelection);
|
|
616
561
|
params.assertBeforeMutation?.();
|
|
562
|
+
if (writePathSelection)
|
|
563
|
+
assertRootWritePathSelectionSync(root, writePathSelection);
|
|
617
564
|
handle = await fs.open(ioPath, createFlags, mode);
|
|
618
565
|
createdForWrite = true;
|
|
619
566
|
}
|
|
@@ -634,49 +581,91 @@ async function openWritableFileInRoot(root, params) {
|
|
|
634
581
|
let createdIdentity = null;
|
|
635
582
|
try {
|
|
636
583
|
const stat = fsSync.fstatSync(handle.fd);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
throw new FsSafeError("not-file", "path is not a regular file under root");
|
|
642
|
-
}
|
|
643
|
-
if (stat.nlink > 1) {
|
|
644
|
-
throw hardlinkedPathNotAllowedError();
|
|
645
|
-
}
|
|
646
|
-
try {
|
|
647
|
-
const lstat = fsSync.lstatSync(ioPath);
|
|
648
|
-
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
649
|
-
throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
|
|
650
|
-
}
|
|
651
|
-
if (!sameFileIdentity(stat, lstat)) {
|
|
652
|
-
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");
|
|
653
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);
|
|
654
607
|
}
|
|
655
608
|
catch (err) {
|
|
656
|
-
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 {
|
|
657
617
|
throw err;
|
|
658
618
|
}
|
|
659
619
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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);
|
|
665
632
|
}
|
|
666
|
-
|
|
667
|
-
|
|
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;
|
|
668
638
|
}
|
|
669
|
-
|
|
639
|
+
const admittedRealPath = admitPathInsideRoot({
|
|
640
|
+
rootPath: root.rootReal,
|
|
641
|
+
candidatePath: realPath,
|
|
642
|
+
rootIdentity: root.rootIdentity,
|
|
643
|
+
});
|
|
644
|
+
if (!admittedRealPath) {
|
|
670
645
|
throw outsideWorkspaceError();
|
|
671
646
|
}
|
|
647
|
+
realPath = admittedRealPath.path;
|
|
648
|
+
realPathForCleanup = realPath;
|
|
649
|
+
assertRootFallbackWritePath(params.expectedWritePath, realPath);
|
|
650
|
+
const writeSelection = writePathSelection
|
|
651
|
+
? createRootWriteSelectionForFd(writePathSelection, handle.fd)
|
|
652
|
+
: undefined;
|
|
672
653
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
673
654
|
// irreversible side effects if a symlink target changes before validation.
|
|
674
655
|
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
|
|
656
|
+
if (writeSelection)
|
|
657
|
+
await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
|
|
675
658
|
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
676
659
|
params.assertBeforeMutation?.();
|
|
660
|
+
if (writeSelection)
|
|
661
|
+
assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
|
|
677
662
|
await handle.truncate(0);
|
|
678
663
|
}
|
|
679
|
-
|
|
664
|
+
if (writeSelection) {
|
|
665
|
+
await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
|
|
666
|
+
assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
|
|
667
|
+
}
|
|
668
|
+
const result = {
|
|
680
669
|
handle,
|
|
681
670
|
containment: "best-effort",
|
|
682
671
|
createdForWrite,
|
|
@@ -684,6 +673,9 @@ async function openWritableFileInRoot(root, params) {
|
|
|
684
673
|
stat,
|
|
685
674
|
[Symbol.asyncDispose]: () => handle.close().catch(() => undefined),
|
|
686
675
|
};
|
|
676
|
+
if (writeSelection)
|
|
677
|
+
retainRootWriteSelection(result, writeSelection);
|
|
678
|
+
return { opened: result, identity };
|
|
687
679
|
}
|
|
688
680
|
catch (err) {
|
|
689
681
|
const cleanupCreatedPath = createdForWrite && err instanceof FsSafeError;
|
|
@@ -698,7 +690,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
698
690
|
async function appendFileInRoot(root, params) {
|
|
699
691
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
700
692
|
try {
|
|
701
|
-
const target = await openWritableFileInRoot(root, {
|
|
693
|
+
const { opened: target, identity } = await openWritableFileInRoot(root, {
|
|
702
694
|
relativePath: params.relativePath,
|
|
703
695
|
mkdir: params.mkdir,
|
|
704
696
|
mode: params.mode,
|
|
@@ -713,7 +705,7 @@ async function appendFileInRoot(root, params) {
|
|
|
713
705
|
const cleanup = __addDisposableResource(env_1, {
|
|
714
706
|
async [Symbol.asyncDispose]() {
|
|
715
707
|
if (!dispatched && target.createdForWrite) {
|
|
716
|
-
await removePathIfIdentityUnchanged(target.realPath,
|
|
708
|
+
await removePathIfIdentityUnchanged(target.realPath, identity);
|
|
717
709
|
}
|
|
718
710
|
},
|
|
719
711
|
}, true);
|
|
@@ -766,14 +758,16 @@ async function appendFileInRoot(root, params) {
|
|
|
766
758
|
}
|
|
767
759
|
async function removePathInRoot(root, params) {
|
|
768
760
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
761
|
+
const removalReceipts = params.recursive ? undefined : new RemovalPathReceipts();
|
|
769
762
|
const resolved = await resolvePinnedPathInRoot(root, {
|
|
770
763
|
relativePath: params.relativePath,
|
|
771
764
|
denyMutations: params.denyMutations,
|
|
772
765
|
mutationSymlinks: params.mutationSymlinks,
|
|
773
766
|
remove: true,
|
|
767
|
+
removalReceipts,
|
|
774
768
|
});
|
|
775
769
|
try {
|
|
776
|
-
await removePathInRootFallback(root, resolved.resolved, params);
|
|
770
|
+
await removePathInRootFallback(root, resolved.resolved, params, removalReceipts);
|
|
777
771
|
}
|
|
778
772
|
catch (error) {
|
|
779
773
|
if (params.recursive)
|
|
@@ -783,9 +777,41 @@ async function removePathInRoot(root, params) {
|
|
|
783
777
|
}
|
|
784
778
|
async function mkdirPathInRoot(root, params) {
|
|
785
779
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
786
|
-
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
|
+
}
|
|
787
810
|
try {
|
|
788
|
-
|
|
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);
|
|
789
815
|
}
|
|
790
816
|
catch (error) {
|
|
791
817
|
throw normalizePinnedPathError(error);
|
|
@@ -822,6 +848,7 @@ async function commitPinnedWriteInRoot(root, pinned, params, input) {
|
|
|
822
848
|
input,
|
|
823
849
|
maxBytes: params.maxBytes,
|
|
824
850
|
rootIdentity: root.rootIdentity,
|
|
851
|
+
mutationAdmission: pinned.mutationAdmission,
|
|
825
852
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
826
853
|
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
827
854
|
verifyingPublication = true;
|
|
@@ -863,6 +890,7 @@ async function copyFileInRoot(root, params) {
|
|
|
863
890
|
let sourceIdentity;
|
|
864
891
|
if (typeof params.source === "string") {
|
|
865
892
|
assertNoNulPathInput(params.source, "source path contains a NUL byte");
|
|
893
|
+
assertNoWindowsPathAlias(params.source, "filesystem", "source path uses a Windows filesystem namespace alias");
|
|
866
894
|
({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
|
|
867
895
|
hardlinks: params.sourceHardlinks,
|
|
868
896
|
}));
|
|
@@ -914,6 +942,7 @@ async function copyFileInRoot(root, params) {
|
|
|
914
942
|
onPublished: observer.onPublished,
|
|
915
943
|
input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
|
|
916
944
|
rootIdentity: root.rootIdentity,
|
|
945
|
+
mutationAdmission: pinned.mutationAdmission,
|
|
917
946
|
});
|
|
918
947
|
}
|
|
919
948
|
catch (error) {
|
|
@@ -933,61 +962,6 @@ async function copyFileInRoot(root, params) {
|
|
|
933
962
|
await source.handle.close().catch(() => { });
|
|
934
963
|
}
|
|
935
964
|
}
|
|
936
|
-
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
937
|
-
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
938
|
-
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
939
|
-
const current = fsSync.lstatSync(targetPath);
|
|
940
|
-
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
|
|
941
|
-
return;
|
|
942
|
-
await fs.unlink(targetPath);
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
|
|
946
|
-
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
947
|
-
relativePath,
|
|
948
|
-
denyMutations,
|
|
949
|
-
mutationSymlinks,
|
|
950
|
-
});
|
|
951
|
-
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
952
|
-
// is rejected upstream.
|
|
953
|
-
const relativeResolved = path.relative(rootReal, resolved);
|
|
954
|
-
if (path.isAbsolute(relativeResolved)) {
|
|
955
|
-
throw outsideWorkspaceError();
|
|
956
|
-
}
|
|
957
|
-
const relativePosix = relativeResolved
|
|
958
|
-
? relativeResolved.split(path.sep).join(path.posix.sep)
|
|
959
|
-
: "";
|
|
960
|
-
const basename = path.posix.basename(relativePosix);
|
|
961
|
-
if (!basename || basename === "." || basename === "/") {
|
|
962
|
-
throw new FsSafeError("invalid-path", "invalid target path");
|
|
963
|
-
}
|
|
964
|
-
// Exclusive publication cannot inherit an existing file's mode. Keep type
|
|
965
|
-
// and alias checks, but never open a competing owner's record just to reject it.
|
|
966
|
-
if (!overwrite) {
|
|
967
|
-
try {
|
|
968
|
-
const existing = fsSync.statSync(resolved);
|
|
969
|
-
if (!existing.isFile())
|
|
970
|
-
throw new FsSafeError("not-file", "not a file");
|
|
971
|
-
if (existing.nlink > 1)
|
|
972
|
-
throw hardlinkedPathNotAllowedError();
|
|
973
|
-
throw new FsSafeError("already-exists", "file already exists");
|
|
974
|
-
}
|
|
975
|
-
catch (error) {
|
|
976
|
-
if (!isNotFoundPathError(error))
|
|
977
|
-
throw error;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
const mode = overwrite
|
|
981
|
-
? await inheritWriteTargetMode({ targetPath: resolved, rootWithSep, requestedMode })
|
|
982
|
-
: requestedMode ?? 0o600;
|
|
983
|
-
return {
|
|
984
|
-
rootReal,
|
|
985
|
-
targetPath: resolved,
|
|
986
|
-
relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
|
|
987
|
-
basename,
|
|
988
|
-
mode,
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
965
|
async function resolvePinnedPathInRoot(root, params) {
|
|
992
966
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
993
967
|
allowRoot: params.allowRoot,
|
|
@@ -996,6 +970,7 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
996
970
|
protectDenyMutationAncestors: params.remove === true,
|
|
997
971
|
relativePath: params.relativePath,
|
|
998
972
|
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
973
|
+
removalReceipts: params.removalReceipts,
|
|
999
974
|
});
|
|
1000
975
|
}
|
|
1001
976
|
async function resolvePinnedOperationPathInRoot(root, params) {
|
|
@@ -1003,6 +978,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
1003
978
|
relativePath: params.relativePath,
|
|
1004
979
|
policy: params.policy,
|
|
1005
980
|
mutationSymlinks: params.mutationSymlinks,
|
|
981
|
+
removalReceipts: params.removalReceipts,
|
|
1006
982
|
});
|
|
1007
983
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
1008
984
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
@@ -1017,30 +993,38 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
1017
993
|
throw outsideWorkspaceError();
|
|
1018
994
|
}
|
|
1019
995
|
const relativePosix = relativeResolved.split(path.sep).join(path.posix.sep);
|
|
1020
|
-
|
|
996
|
+
const admittedCanonicalPath = admitPathInsideRoot({
|
|
997
|
+
rootPath: resolved.rootReal,
|
|
998
|
+
candidatePath: resolved.canonicalPath,
|
|
999
|
+
rootIdentity: root.rootIdentity,
|
|
1000
|
+
});
|
|
1001
|
+
if (!admittedCanonicalPath) {
|
|
1021
1002
|
throw outsideWorkspaceError();
|
|
1022
1003
|
}
|
|
1004
|
+
resolved.canonicalPath = admittedCanonicalPath.path;
|
|
1023
1005
|
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
|
|
1024
1006
|
protectAncestors: params.protectDenyMutationAncestors,
|
|
1025
1007
|
});
|
|
1026
1008
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
1027
1009
|
}
|
|
1028
1010
|
async function resolvePinnedRootPathInRoot(root, params) {
|
|
1029
|
-
await assertRootIdentityCurrent(root);
|
|
1011
|
+
await assertRootIdentityCurrent(root, params.removalReceipts?.observeRoot);
|
|
1030
1012
|
const rootReal = root.rootReal;
|
|
1031
1013
|
let resolved;
|
|
1032
1014
|
try {
|
|
1033
1015
|
const expandedPath = await expandRelativePathWithHome(params.relativePath);
|
|
1034
|
-
|
|
1016
|
+
const resolution = {
|
|
1035
1017
|
absolutePath: path.isAbsolute(expandedPath)
|
|
1036
1018
|
? expandedPath
|
|
1037
1019
|
: `${ensureTrailingSep(rootReal)}${expandedPath}`,
|
|
1038
1020
|
rootPath: rootReal,
|
|
1039
1021
|
rootCanonicalPath: rootReal,
|
|
1022
|
+
rootIdentity: root.rootIdentity,
|
|
1040
1023
|
boundaryLabel: "root",
|
|
1041
1024
|
policy: params.policy,
|
|
1042
1025
|
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1043
|
-
}
|
|
1026
|
+
};
|
|
1027
|
+
resolved = await (params.removalReceipts ? resolveRootPathForRemoval(resolution, params.removalReceipts) : resolveRootPath(resolution));
|
|
1044
1028
|
}
|
|
1045
1029
|
catch (err) {
|
|
1046
1030
|
if (err instanceof FsSafeError && err.code === "symlink")
|
|
@@ -1054,44 +1038,20 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
1054
1038
|
canonicalPath: resolved.canonicalPath,
|
|
1055
1039
|
};
|
|
1056
1040
|
}
|
|
1057
|
-
async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
|
|
1058
|
-
await mkdirPathComponentsWithGuards({
|
|
1059
|
-
rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
|
|
1060
|
-
rejectSymlinks,
|
|
1061
|
-
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
1041
|
async function statPathFallback(root, relativePath) {
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
await
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (isNotFoundPathError(error)) {
|
|
1073
|
-
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
1074
|
-
}
|
|
1075
|
-
throw error;
|
|
1076
|
-
}
|
|
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);
|
|
1077
1049
|
}
|
|
1078
1050
|
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Keep this preflight separate from the pinned resolutions in movePathFallback:
|
|
1084
|
-
// mutation denials must take precedence over source alias or identity failures.
|
|
1085
|
-
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
1086
|
-
aliasErrorCode: "path-alias",
|
|
1087
|
-
allowFinalSymlink: true,
|
|
1088
|
-
});
|
|
1089
|
-
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1090
|
-
const target = await resolvePathInRoot(root, params.toRelative, {
|
|
1091
|
-
aliasErrorCode: "path-alias",
|
|
1092
|
-
allowFinalSymlink: true,
|
|
1093
|
-
});
|
|
1094
|
-
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
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);
|
|
1095
1055
|
}
|
|
1096
1056
|
async function movePathFallback(root, params) {
|
|
1097
1057
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
@@ -1100,11 +1060,12 @@ async function movePathFallback(root, params) {
|
|
|
1100
1060
|
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1101
1061
|
});
|
|
1102
1062
|
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1103
|
-
await resolvePinnedRootPathInRoot(root, {
|
|
1063
|
+
const pinnedSource = await resolvePinnedRootPathInRoot(root, {
|
|
1104
1064
|
relativePath: params.fromRelative,
|
|
1105
1065
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1106
1066
|
mutationSymlinks: params.mutationSymlinks,
|
|
1107
1067
|
});
|
|
1068
|
+
let pinnedTarget;
|
|
1108
1069
|
const target = await resolveGuardedWritePathInRoot(root, {
|
|
1109
1070
|
relativePath: params.toRelative,
|
|
1110
1071
|
denyMutations: params.denyMutations,
|
|
@@ -1112,10 +1073,12 @@ async function movePathFallback(root, params) {
|
|
|
1112
1073
|
allowFinalSymlink: true,
|
|
1113
1074
|
protectDeniedAncestors: true,
|
|
1114
1075
|
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
1115
|
-
await resolvePinnedRootPathInRoot(root, {
|
|
1076
|
+
pinnedTarget = await resolvePinnedRootPathInRoot(root, {
|
|
1116
1077
|
relativePath: params.toRelative,
|
|
1117
1078
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
1118
1079
|
});
|
|
1080
|
+
if (!params.overwrite)
|
|
1081
|
+
return true;
|
|
1119
1082
|
let targetStat;
|
|
1120
1083
|
try {
|
|
1121
1084
|
targetStat = fsSync.lstatSync(resolvedTarget.resolved);
|
|
@@ -1146,18 +1109,16 @@ async function movePathFallback(root, params) {
|
|
|
1146
1109
|
throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
1147
1110
|
}
|
|
1148
1111
|
if (!params.overwrite) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
throw new FsSafeError("already-exists", "destination exists");
|
|
1152
|
-
}
|
|
1153
|
-
catch (error) {
|
|
1154
|
-
if (error instanceof FsSafeError) {
|
|
1155
|
-
throw error;
|
|
1156
|
-
}
|
|
1157
|
-
if (!isNotFoundPathError(error)) {
|
|
1158
|
-
throw error;
|
|
1159
|
-
}
|
|
1112
|
+
if (!pinnedTarget) {
|
|
1113
|
+
throw new FsSafeError("path-mismatch", "destination admission was not completed");
|
|
1160
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;
|
|
1161
1122
|
}
|
|
1162
1123
|
const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
|
|
1163
1124
|
const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
|
|
@@ -1190,11 +1151,22 @@ async function movePathFallback(root, params) {
|
|
|
1190
1151
|
}
|
|
1191
1152
|
}
|
|
1192
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) {
|
|
1193
1165
|
if (params.overwrite === false) {
|
|
1194
|
-
await writeMissingFileFallback(root, params);
|
|
1166
|
+
await writeMissingFileFallback(root, params, expectedWritePath);
|
|
1195
1167
|
return;
|
|
1196
1168
|
}
|
|
1197
|
-
const target = await openWritableFileInRoot(root, {
|
|
1169
|
+
const { opened: target, identity: targetIdentity } = await openWritableFileInRoot(root, {
|
|
1198
1170
|
relativePath: params.relativePath,
|
|
1199
1171
|
mkdir: params.mkdir,
|
|
1200
1172
|
// Private, writable placeholder: Windows cannot rename over a read-only file.
|
|
@@ -1203,13 +1175,21 @@ async function writeFileFallback(root, params) {
|
|
|
1203
1175
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
1204
1176
|
mutationSymlinks: params.mutationSymlinks,
|
|
1205
1177
|
truncateExisting: false,
|
|
1178
|
+
expectedWritePath,
|
|
1206
1179
|
});
|
|
1207
|
-
const
|
|
1180
|
+
const policyEnabled = params.denyMutations !== undefined || params.mutationSymlinks !== undefined;
|
|
1181
|
+
const retainedSelection = policyEnabled ? takeRootWriteSelection(target) : undefined;
|
|
1182
|
+
const destinationPath = retainedSelection?.selectedPath ?? target.realPath;
|
|
1208
1183
|
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
1209
|
-
|
|
1184
|
+
if (policyEnabled && !retainedSelection) {
|
|
1210
1185
|
await target.handle.close().catch(() => undefined);
|
|
1211
|
-
throw
|
|
1212
|
-
}
|
|
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
|
+
});
|
|
1213
1193
|
let tempPath = null;
|
|
1214
1194
|
let unregisterTempPath = null;
|
|
1215
1195
|
let writtenHandle;
|
|
@@ -1218,9 +1198,15 @@ async function writeFileFallback(root, params) {
|
|
|
1218
1198
|
let published = false;
|
|
1219
1199
|
try {
|
|
1220
1200
|
if (target.createdForWrite)
|
|
1221
|
-
placeholderIdentity =
|
|
1201
|
+
placeholderIdentity = targetIdentity;
|
|
1222
1202
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1203
|
+
if (retainedSelection) {
|
|
1204
|
+
await refreshRetainedRootWriteAdmission(root, retainedSelection, true, target.handle.fd);
|
|
1205
|
+
}
|
|
1223
1206
|
params.assertBeforeMutation?.();
|
|
1207
|
+
if (retainedSelection) {
|
|
1208
|
+
assertRootWriteSelectionSync(root, retainedSelection, true, target.handle.fd);
|
|
1209
|
+
}
|
|
1224
1210
|
writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
|
|
1225
1211
|
writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
|
|
1226
1212
|
unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
|
|
@@ -1244,41 +1230,27 @@ async function writeFileFallback(root, params) {
|
|
|
1244
1230
|
}
|
|
1245
1231
|
// Windows cannot replace a destination while its old handle remains open.
|
|
1246
1232
|
await target.handle.close();
|
|
1233
|
+
if (retainedSelection) {
|
|
1234
|
+
await refreshRetainedRootWriteAdmission(root, retainedSelection, !target.createdForWrite);
|
|
1235
|
+
}
|
|
1247
1236
|
assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
|
|
1248
1237
|
params.assertBeforeMutation?.();
|
|
1238
|
+
if (retainedSelection) {
|
|
1239
|
+
assertRootWriteSelectionSync(root, retainedSelection, !target.createdForWrite);
|
|
1240
|
+
}
|
|
1249
1241
|
await fs.rename(commitTempPath, destinationPath);
|
|
1250
1242
|
tempPath = null;
|
|
1251
1243
|
published = true;
|
|
1252
1244
|
});
|
|
1253
1245
|
unregisterTempPath();
|
|
1254
1246
|
unregisterTempPath = null;
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
});
|
|
1263
|
-
throw error;
|
|
1264
|
-
}
|
|
1265
|
-
if (params.durable !== false)
|
|
1266
|
-
await writtenHandle.sync();
|
|
1267
|
-
try {
|
|
1268
|
-
await verifyAtomicWriteResult({
|
|
1269
|
-
root,
|
|
1270
|
-
targetPath: destinationPath,
|
|
1271
|
-
expectedIdentity: writtenIdentity,
|
|
1272
|
-
fd: writtenHandle.fd,
|
|
1273
|
-
parentGuard: destinationGuard,
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
catch (err) {
|
|
1277
|
-
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
1278
|
-
throw err;
|
|
1279
|
-
}
|
|
1280
|
-
if (params.durable !== false)
|
|
1281
|
-
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
|
+
});
|
|
1282
1254
|
}
|
|
1283
1255
|
finally {
|
|
1284
1256
|
if (!published && target.createdForWrite) {
|
|
@@ -1296,16 +1268,31 @@ async function writeFileFallback(root, params) {
|
|
|
1296
1268
|
unregisterTempPath?.();
|
|
1297
1269
|
}
|
|
1298
1270
|
}
|
|
1299
|
-
async function writeMissingFileFallback(root, params) {
|
|
1300
|
-
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, {
|
|
1301
1280
|
relativePath: params.relativePath,
|
|
1302
|
-
denyMutations: params.denyMutations,
|
|
1303
|
-
mutationSymlinks: params.mutationSymlinks,
|
|
1304
1281
|
});
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
:
|
|
1308
|
-
|
|
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 });
|
|
1309
1296
|
let created = false;
|
|
1310
1297
|
let createdIdentity;
|
|
1311
1298
|
let writtenHandle;
|
|
@@ -1314,6 +1301,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1314
1301
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1315
1302
|
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1316
1303
|
params.assertBeforeMutation?.();
|
|
1304
|
+
if (mutationAdmission)
|
|
1305
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
1317
1306
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
|
|
1318
1307
|
writtenHandle = handle;
|
|
1319
1308
|
created = true;
|
|
@@ -1324,6 +1313,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1324
1313
|
assertBeforeMutation: () => {
|
|
1325
1314
|
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1326
1315
|
params.assertBeforeMutation?.();
|
|
1316
|
+
if (mutationAdmission)
|
|
1317
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
1327
1318
|
},
|
|
1328
1319
|
});
|
|
1329
1320
|
if (params.durable !== false)
|