@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-paths.js
CHANGED
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { formatErrorDetail } from "./error-detail.js";
|
|
5
3
|
import { FsSafeError } from "./errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import { root as openRoot } from "./root.js";
|
|
4
|
+
import { isNotFoundPathError, isPathInside, } from "./path.js";
|
|
8
5
|
import { realpathSync } from "./realpath.js";
|
|
6
|
+
import { root as openRoot } from "./root.js";
|
|
7
|
+
import { hasWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
8
|
+
import { resolvePathWithinRoot } from "./root-paths-lexical.js";
|
|
9
|
+
import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
|
|
10
|
+
import { assertNoSymlinkSegments, ensureDirectoryWithinRoot, resolveNearestExistingPath, } from "./root-directory.js";
|
|
11
|
+
export { resolvePathWithinRoot } from "./root-paths-lexical.js";
|
|
12
|
+
export { ensureDirectoryWithinRoot } from "./root-directory.js";
|
|
9
13
|
function invalidPath(scopeLabel) {
|
|
10
14
|
return {
|
|
11
15
|
ok: false,
|
|
12
16
|
error: `Invalid path: must stay within ${scopeLabel}`,
|
|
13
17
|
};
|
|
14
18
|
}
|
|
15
|
-
|
|
16
|
-
if (process.platform !== "win32") {
|
|
17
|
-
return candidatePath !== rootDir && isPathInside(rootDir, candidatePath);
|
|
18
|
-
}
|
|
19
|
-
const relative = path.relative(rootDir, candidatePath);
|
|
20
|
-
return Boolean(relative) && !isPathRelativeEscape(relative);
|
|
21
|
-
}
|
|
19
|
+
const INVALID_REAL_PATH = Symbol("invalid-real-path");
|
|
22
20
|
async function resolveRealPathIfExists(targetPath) {
|
|
21
|
+
if (hasWindowsPathAlias(targetPath, "filesystem"))
|
|
22
|
+
return INVALID_REAL_PATH;
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
const realPath = realpathSync.native(pathForWindowsFilesystem(targetPath));
|
|
25
|
+
return hasWindowsPathAlias(realPath, "filesystem") ? INVALID_REAL_PATH : realPath;
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
28
|
return undefined;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
async function resolveTrustedRootRealPath(rootDir) {
|
|
32
|
+
if (hasWindowsPathAlias(rootDir, "filesystem"))
|
|
33
|
+
return undefined;
|
|
31
34
|
try {
|
|
32
|
-
const
|
|
35
|
+
const operationPath = pathForWindowsFilesystem(rootDir);
|
|
36
|
+
const rootLstat = fsSync.lstatSync(operationPath);
|
|
33
37
|
if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) {
|
|
34
38
|
return undefined;
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
const realPath = realpathSync.native(operationPath);
|
|
41
|
+
return hasWindowsPathAlias(realPath, "filesystem") ? undefined : realPath;
|
|
37
42
|
}
|
|
38
43
|
catch {
|
|
39
44
|
return undefined;
|
|
@@ -41,7 +46,8 @@ async function resolveTrustedRootRealPath(rootDir) {
|
|
|
41
46
|
}
|
|
42
47
|
async function validateCanonicalPathWithinRoot(params) {
|
|
43
48
|
try {
|
|
44
|
-
const
|
|
49
|
+
const operationPath = pathForWindowsFilesystem(params.candidatePath);
|
|
50
|
+
const candidateLstat = fsSync.lstatSync(operationPath);
|
|
45
51
|
if (candidateLstat.isSymbolicLink()) {
|
|
46
52
|
return "invalid";
|
|
47
53
|
}
|
|
@@ -54,41 +60,33 @@ async function validateCanonicalPathWithinRoot(params) {
|
|
|
54
60
|
if (params.expect === "file" && candidateLstat.nlink > 1) {
|
|
55
61
|
return "invalid";
|
|
56
62
|
}
|
|
57
|
-
const candidateRealPath = realpathSync.native(
|
|
63
|
+
const candidateRealPath = realpathSync.native(operationPath);
|
|
64
|
+
if (hasWindowsPathAlias(candidateRealPath, "filesystem"))
|
|
65
|
+
return "invalid";
|
|
58
66
|
return isPathInside(params.rootRealPath, candidateRealPath) ? "ok" : "invalid";
|
|
59
67
|
}
|
|
60
68
|
catch (err) {
|
|
61
69
|
return isNotFoundPathError(err) ? "not-found" : "invalid";
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
|
-
export function resolvePathWithinRoot(params) {
|
|
65
|
-
const root = path.resolve(params.rootDir);
|
|
66
|
-
const raw = params.requestedPath.trim();
|
|
67
|
-
if (!raw) {
|
|
68
|
-
if (!params.defaultFileName) {
|
|
69
|
-
return { ok: false, error: "path is required" };
|
|
70
|
-
}
|
|
71
|
-
const defaultPath = path.resolve(root, params.defaultFileName);
|
|
72
|
-
if (!pathStaysWithinRoot(root, defaultPath)) {
|
|
73
|
-
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
74
|
-
}
|
|
75
|
-
return { ok: true, path: defaultPath };
|
|
76
|
-
}
|
|
77
|
-
const resolved = path.resolve(root, raw);
|
|
78
|
-
if (!pathStaysWithinRoot(root, resolved)) {
|
|
79
|
-
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
80
|
-
}
|
|
81
|
-
return { ok: true, path: resolved };
|
|
82
|
-
}
|
|
83
72
|
export async function resolveWritablePathWithinRoot(params) {
|
|
84
|
-
const
|
|
73
|
+
const rootDirInput = params.rootDir;
|
|
74
|
+
const requestedPathInput = params.requestedPath;
|
|
75
|
+
const scopeLabel = params.scopeLabel;
|
|
76
|
+
const defaultFileName = params.defaultFileName;
|
|
77
|
+
const lexical = resolvePathWithinRoot({
|
|
78
|
+
rootDir: rootDirInput,
|
|
79
|
+
requestedPath: requestedPathInput,
|
|
80
|
+
scopeLabel,
|
|
81
|
+
defaultFileName,
|
|
82
|
+
});
|
|
85
83
|
if (!lexical.ok) {
|
|
86
84
|
return lexical;
|
|
87
85
|
}
|
|
88
|
-
const rootDir =
|
|
86
|
+
const rootDir = resolvePathPreservingWindowsRoot(rootDirInput);
|
|
89
87
|
const rootRealPath = await resolveTrustedRootRealPath(rootDir);
|
|
90
88
|
if (!rootRealPath) {
|
|
91
|
-
return invalidPath(
|
|
89
|
+
return invalidPath(scopeLabel);
|
|
92
90
|
}
|
|
93
91
|
const requestedPath = lexical.path;
|
|
94
92
|
const parentDir = path.dirname(requestedPath);
|
|
@@ -98,7 +96,7 @@ export async function resolveWritablePathWithinRoot(params) {
|
|
|
98
96
|
expect: "directory",
|
|
99
97
|
});
|
|
100
98
|
if (parentStatus !== "ok") {
|
|
101
|
-
return invalidPath(
|
|
99
|
+
return invalidPath(scopeLabel);
|
|
102
100
|
}
|
|
103
101
|
const targetStatus = await validateCanonicalPathWithinRoot({
|
|
104
102
|
rootRealPath,
|
|
@@ -106,139 +104,33 @@ export async function resolveWritablePathWithinRoot(params) {
|
|
|
106
104
|
expect: "file",
|
|
107
105
|
});
|
|
108
106
|
if (targetStatus === "invalid") {
|
|
109
|
-
return invalidPath(
|
|
107
|
+
return invalidPath(scopeLabel);
|
|
110
108
|
}
|
|
111
109
|
return lexical;
|
|
112
110
|
}
|
|
113
|
-
async function resolveNearestExistingPath(targetPath) {
|
|
114
|
-
let current = path.resolve(targetPath);
|
|
115
|
-
while (true) {
|
|
116
|
-
try {
|
|
117
|
-
fsSync.lstatSync(current);
|
|
118
|
-
return current;
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
if (!isNotFoundPathError(err)) {
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const parent = path.dirname(current);
|
|
126
|
-
if (parent === current) {
|
|
127
|
-
throw new Error(`failed to resolve existing path for ${targetPath}`);
|
|
128
|
-
}
|
|
129
|
-
current = parent;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async function assertNoSymlinkSegments(params) {
|
|
133
|
-
const relative = path.relative(params.rootDir, params.targetPath);
|
|
134
|
-
if (isPathRelativeEscape(relative)) {
|
|
135
|
-
throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
|
|
136
|
-
}
|
|
137
|
-
let current = params.rootDir;
|
|
138
|
-
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
139
|
-
current = path.join(current, segment);
|
|
140
|
-
try {
|
|
141
|
-
const stat = fsSync.lstatSync(current);
|
|
142
|
-
if (stat.isSymbolicLink()) {
|
|
143
|
-
throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
|
|
144
|
-
}
|
|
145
|
-
if (!stat.isDirectory()) {
|
|
146
|
-
throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
if (isNotFoundPathError(err)) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
export async function ensureDirectoryWithinRoot(params) {
|
|
158
|
-
const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
|
|
159
|
-
try {
|
|
160
|
-
assertNoNulPathInput(params.rootDir);
|
|
161
|
-
assertNoNulPathInput(params.requestedPath);
|
|
162
|
-
if (!params.requestedPath.trim())
|
|
163
|
-
assertNoNulPathInput(params.defaultDirName ?? "");
|
|
164
|
-
const lexical = resolvePathWithinRoot({
|
|
165
|
-
...params, scopeLabel, defaultFileName: params.defaultDirName,
|
|
166
|
-
});
|
|
167
|
-
if (!lexical.ok)
|
|
168
|
-
return lexical;
|
|
169
|
-
const rootDir = path.resolve(params.rootDir);
|
|
170
|
-
const targetPath = lexical.path;
|
|
171
|
-
const rootStat = fsSync.lstatSync(rootDir);
|
|
172
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
173
|
-
return invalidPath(scopeLabel);
|
|
174
|
-
}
|
|
175
|
-
await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
|
|
176
|
-
const rootReal = realpathSync.native(rootDir);
|
|
177
|
-
const nearestExistingPath = await resolveNearestExistingPath(targetPath);
|
|
178
|
-
const nearestExistingReal = realpathSync.native(nearestExistingPath);
|
|
179
|
-
if (!isPathInside(rootReal, nearestExistingReal)) {
|
|
180
|
-
return invalidPath(scopeLabel);
|
|
181
|
-
}
|
|
182
|
-
const relative = path.relative(rootDir, targetPath);
|
|
183
|
-
let current = rootDir;
|
|
184
|
-
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
185
|
-
current = path.join(current, segment);
|
|
186
|
-
while (true) {
|
|
187
|
-
try {
|
|
188
|
-
const stat = fsSync.lstatSync(current);
|
|
189
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
190
|
-
return invalidPath(scopeLabel);
|
|
191
|
-
}
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
if (!isNotFoundPathError(err)) {
|
|
196
|
-
throw err;
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
await fs.mkdir(current, { mode: params.mode });
|
|
200
|
-
}
|
|
201
|
-
catch (mkdirErr) {
|
|
202
|
-
if (hasNodeErrorCode(mkdirErr, "EEXIST")) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
throw mkdirErr;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const currentReal = realpathSync.native(current);
|
|
210
|
-
if (!isPathInside(rootReal, currentReal)) {
|
|
211
|
-
return invalidPath(scopeLabel);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const targetReal = realpathSync.native(targetPath);
|
|
215
|
-
if (!isPathInside(rootReal, targetReal)) {
|
|
216
|
-
return invalidPath(scopeLabel);
|
|
217
|
-
}
|
|
218
|
-
return { ok: true, path: targetPath };
|
|
219
|
-
}
|
|
220
|
-
catch (cause) {
|
|
221
|
-
if ((cause instanceof FsSafeError && cause.category === "policy") ||
|
|
222
|
-
hasNodeErrorCode(cause, "ENOTDIR")) {
|
|
223
|
-
return invalidPath(scopeLabel);
|
|
224
|
-
}
|
|
225
|
-
// Native messages include unbounded paths; display only bounded errno metadata.
|
|
226
|
-
const code = isNodeError(cause) && typeof cause.code === "string"
|
|
227
|
-
? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
|
|
228
|
-
const syscall = isNodeError(cause) && typeof cause.syscall === "string"
|
|
229
|
-
? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
|
|
230
|
-
const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
|
|
231
|
-
return { ok: false, error: diagnostic.message, diagnostic };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
111
|
export function resolvePathsWithinRoot(params) {
|
|
235
112
|
const resolvedPaths = [];
|
|
113
|
+
let previousRootDir;
|
|
114
|
+
let previousResolvedRoot = "";
|
|
236
115
|
for (const raw of params.requestedPaths) {
|
|
237
|
-
const
|
|
116
|
+
const request = {
|
|
238
117
|
rootDir: params.rootDir,
|
|
239
118
|
requestedPath: raw,
|
|
240
119
|
scopeLabel: params.scopeLabel,
|
|
241
|
-
}
|
|
120
|
+
};
|
|
121
|
+
// Each lexical request owns its admitted root without consuming later items.
|
|
122
|
+
if (typeof request.rootDir !== "string")
|
|
123
|
+
path.resolve(request.rootDir);
|
|
124
|
+
if (hasWindowsPathAlias(request.rootDir, "filesystem"))
|
|
125
|
+
return invalidPath(request.scopeLabel);
|
|
126
|
+
// Relative roots may follow cwd changes made by an input iterator or getter.
|
|
127
|
+
const resolvedRoot = process.platform !== "win32" && typeof request.rootDir === "string" &&
|
|
128
|
+
request.rootDir.startsWith("/") && request.rootDir === previousRootDir
|
|
129
|
+
? previousResolvedRoot
|
|
130
|
+
: resolvePathPreservingWindowsRoot(request.rootDir);
|
|
131
|
+
previousRootDir = request.rootDir;
|
|
132
|
+
previousResolvedRoot = resolvedRoot;
|
|
133
|
+
const pathResult = resolvePathWithinNormalizedRoot(request, resolvedRoot);
|
|
242
134
|
if (!pathResult.ok) {
|
|
243
135
|
return { ok: false, error: pathResult.error };
|
|
244
136
|
}
|
|
@@ -253,10 +145,11 @@ export async function resolveStrictExistingPathsWithinRoot(params) {
|
|
|
253
145
|
return await resolveCheckedPathsWithinRoot(params, false);
|
|
254
146
|
}
|
|
255
147
|
export function pathScope(rootDir, options) {
|
|
256
|
-
const
|
|
148
|
+
const label = options.label;
|
|
149
|
+
const base = { rootDir, scopeLabel: label };
|
|
257
150
|
return {
|
|
258
151
|
rootDir,
|
|
259
|
-
label
|
|
152
|
+
label,
|
|
260
153
|
resolve: (requestedPath, pathOptions) => resolvePathWithinRoot({
|
|
261
154
|
...base,
|
|
262
155
|
requestedPath,
|
|
@@ -288,8 +181,19 @@ export function pathScope(rootDir, options) {
|
|
|
288
181
|
};
|
|
289
182
|
}
|
|
290
183
|
async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
291
|
-
const
|
|
292
|
-
const
|
|
184
|
+
const rootDirInput = params.rootDir;
|
|
185
|
+
const scopeLabel = params.scopeLabel;
|
|
186
|
+
if (hasWindowsPathAlias(rootDirInput, "filesystem")) {
|
|
187
|
+
return invalidPath(scopeLabel);
|
|
188
|
+
}
|
|
189
|
+
const requestedPaths = [...params.requestedPaths];
|
|
190
|
+
const rootDir = resolvePathPreservingWindowsRoot(rootDirInput);
|
|
191
|
+
if (hasWindowsPathAlias(rootDir, "filesystem"))
|
|
192
|
+
return invalidPath(scopeLabel);
|
|
193
|
+
const rootRealPathResult = await resolveRealPathIfExists(rootDir);
|
|
194
|
+
if (rootRealPathResult === INVALID_REAL_PATH)
|
|
195
|
+
return invalidPath(scopeLabel);
|
|
196
|
+
const rootRealPath = rootRealPathResult;
|
|
293
197
|
const root = rootRealPath ? await openRoot(rootDir) : undefined;
|
|
294
198
|
const isInRoot = (relativePath) => Boolean(relativePath) &&
|
|
295
199
|
relativePath !== ".." &&
|
|
@@ -300,7 +204,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
300
204
|
const lexicalPathResult = resolvePathWithinRoot({
|
|
301
205
|
rootDir,
|
|
302
206
|
requestedPath,
|
|
303
|
-
scopeLabel
|
|
207
|
+
scopeLabel,
|
|
304
208
|
});
|
|
305
209
|
if (lexicalPathResult.ok) {
|
|
306
210
|
return {
|
|
@@ -309,11 +213,17 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
309
213
|
fallbackPath: lexicalPathResult.path,
|
|
310
214
|
};
|
|
311
215
|
}
|
|
312
|
-
if (!rootRealPath ||
|
|
216
|
+
if (!rootRealPath ||
|
|
217
|
+
!raw ||
|
|
218
|
+
!path.isAbsolute(raw) ||
|
|
219
|
+
hasWindowsPathAlias(raw, "filesystem")) {
|
|
313
220
|
return lexicalPathResult;
|
|
314
221
|
}
|
|
315
222
|
try {
|
|
316
|
-
const resolvedExistingPath = realpathSync.native(raw);
|
|
223
|
+
const resolvedExistingPath = realpathSync.native(pathForWindowsFilesystem(raw));
|
|
224
|
+
if (hasWindowsPathAlias(resolvedExistingPath, "filesystem")) {
|
|
225
|
+
return lexicalPathResult;
|
|
226
|
+
}
|
|
317
227
|
const relativePath = path.relative(rootRealPath, resolvedExistingPath);
|
|
318
228
|
if (!isInRoot(relativePath)) {
|
|
319
229
|
return lexicalPathResult;
|
|
@@ -329,7 +239,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
329
239
|
}
|
|
330
240
|
};
|
|
331
241
|
const resolvedPaths = [];
|
|
332
|
-
for (const raw of
|
|
242
|
+
for (const raw of requestedPaths) {
|
|
333
243
|
const pathResult = await resolveExistingRelativePath(raw);
|
|
334
244
|
if (!pathResult.ok) {
|
|
335
245
|
return { ok: false, error: pathResult.error };
|
|
@@ -352,16 +262,17 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
352
262
|
await assertNoSymlinkSegments({
|
|
353
263
|
rootDir,
|
|
354
264
|
targetPath: pathResult.fallbackPath,
|
|
355
|
-
scopeLabel
|
|
265
|
+
scopeLabel,
|
|
356
266
|
});
|
|
357
267
|
const existingPath = await resolveNearestExistingPath(pathResult.fallbackPath);
|
|
358
|
-
const existingRealPath = realpathSync.native(existingPath);
|
|
359
|
-
if (
|
|
360
|
-
|
|
268
|
+
const existingRealPath = realpathSync.native(pathForWindowsFilesystem(existingPath));
|
|
269
|
+
if (hasWindowsPathAlias(existingRealPath, "filesystem") ||
|
|
270
|
+
!isPathInside(rootRealPath, existingRealPath)) {
|
|
271
|
+
return invalidPath(scopeLabel);
|
|
361
272
|
}
|
|
362
273
|
}
|
|
363
274
|
catch {
|
|
364
|
-
return invalidPath(
|
|
275
|
+
return invalidPath(scopeLabel);
|
|
365
276
|
}
|
|
366
277
|
resolvedPaths.push(pathResult.fallbackPath);
|
|
367
278
|
continue;
|
|
@@ -369,12 +280,12 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
369
280
|
if (err instanceof FsSafeError && err.code === "outside-workspace") {
|
|
370
281
|
return {
|
|
371
282
|
ok: false,
|
|
372
|
-
error: `File is outside ${
|
|
283
|
+
error: `File is outside ${scopeLabel}`,
|
|
373
284
|
};
|
|
374
285
|
}
|
|
375
286
|
return {
|
|
376
287
|
ok: false,
|
|
377
|
-
error: `Invalid path: must stay within ${
|
|
288
|
+
error: `Invalid path: must stay within ${scopeLabel} and be a regular non-symlink file`,
|
|
378
289
|
};
|
|
379
290
|
}
|
|
380
291
|
finally {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import type { FileHandle } from "node:fs/promises";
|
|
3
|
+
import { type RootContext } from "./root-context.js";
|
|
4
|
+
import type { HardlinkPolicy } from "./root-options.js";
|
|
5
|
+
import type { SymlinkPolicy } from "./root-symlink-policy.js";
|
|
6
|
+
export declare function inspectOpenedPathIdentitySync(filePath: string, symlinks: SymlinkPolicy | undefined): BigIntStats;
|
|
7
|
+
type OwnedRootReadHandle = {
|
|
8
|
+
handle: FileHandle;
|
|
9
|
+
stat: {
|
|
10
|
+
nlink: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function admitRootReadHandle<T extends OwnedRootReadHandle>(params: {
|
|
14
|
+
root: RootContext;
|
|
15
|
+
filePath: string;
|
|
16
|
+
opened: T;
|
|
17
|
+
identity: BigIntStats;
|
|
18
|
+
hardlinks?: HardlinkPolicy;
|
|
19
|
+
symlinks?: SymlinkPolicy;
|
|
20
|
+
beforeFinalFence?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
21
|
+
afterPathIdentityCheck?: (filePath: string, handle: FileHandle) => void;
|
|
22
|
+
}): Promise<T & {
|
|
23
|
+
realPath: string;
|
|
24
|
+
}>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=root-read-admission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-read-admission.d.ts","sourceRoot":"","sources":["../src/root-read-admission.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAmB9D,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,WAAW,CAQb;AA6CD,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,mBAAmB,EAAE,MAAM,EAAE;IAC/E,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClF,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACzE,GAAG,OAAO,CAAC,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8BpC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { openedPathResolutionError, recordOpenedFileFailure } from "./opened-file-failure.js";
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
6
|
+
import { assertRootIdentityCurrentSync } from "./root-context.js";
|
|
7
|
+
import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
8
|
+
import { realpathSync } from "./realpath.js";
|
|
9
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
10
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
11
|
+
function resolveFinalOpenedRealPath(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync.native(filePath);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
// Windows can report a deleted-but-open file through either resolver
|
|
17
|
+
// failure. Only the same-descriptor unlink proof may consume this receipt.
|
|
18
|
+
if (process.platform === "win32" && error instanceof Error &&
|
|
19
|
+
["EPERM", "EBADF"].includes(error.code ?? "")) {
|
|
20
|
+
throw openedPathResolutionError(error);
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function inspectOpenedPathIdentitySync(filePath, symlinks) {
|
|
26
|
+
const stat = symlinks === "follow-within-root"
|
|
27
|
+
? fsSync.statSync(filePath, { bigint: true })
|
|
28
|
+
: fsSync.lstatSync(filePath, { bigint: true });
|
|
29
|
+
if (stat.isSymbolicLink() && symlinks !== "follow-within-root") {
|
|
30
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
31
|
+
}
|
|
32
|
+
return stat;
|
|
33
|
+
}
|
|
34
|
+
function assertRootReadAdmissionCurrent(params) {
|
|
35
|
+
assertRootIdentityCurrentSync(params.root);
|
|
36
|
+
const admittedPath = (() => {
|
|
37
|
+
try {
|
|
38
|
+
inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(params.filePath, params.symlinks), params.identity);
|
|
39
|
+
const canonicalPath = resolveFinalOpenedRealPath(params.filePath);
|
|
40
|
+
assertNoWindowsPathAlias(canonicalPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
|
|
41
|
+
const admittedRealPath = admitPathInsideRoot({
|
|
42
|
+
rootPath: params.root.rootReal,
|
|
43
|
+
candidatePath: canonicalPath,
|
|
44
|
+
rootIdentity: params.root.rootIdentity,
|
|
45
|
+
});
|
|
46
|
+
if (!admittedRealPath) {
|
|
47
|
+
throw openedPathResolutionError(outsideWorkspaceError());
|
|
48
|
+
}
|
|
49
|
+
// This is a fresh no-follow observation even when canonicalPath and
|
|
50
|
+
// filePath have the same spelling; realpath may have raced a replacement.
|
|
51
|
+
inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(canonicalPath, undefined), params.identity);
|
|
52
|
+
return admittedRealPath.path;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw isNotFoundPathError(error)
|
|
56
|
+
? openedPathResolutionError(fileNotFoundError())
|
|
57
|
+
: error;
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
params.afterPathIdentityCheck?.(params.filePath, params.handle);
|
|
61
|
+
assertRootIdentityCurrentSync(params.root);
|
|
62
|
+
return admittedPath;
|
|
63
|
+
}
|
|
64
|
+
export async function admitRootReadHandle(params) {
|
|
65
|
+
try {
|
|
66
|
+
if (params.hardlinks !== "allow" && params.opened.stat.nlink > 1) {
|
|
67
|
+
throw hardlinkedPathNotAllowedError();
|
|
68
|
+
}
|
|
69
|
+
if (params.beforeFinalFence) {
|
|
70
|
+
await params.beforeFinalFence(params.filePath, params.opened.handle);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const finalRealPath = assertRootReadAdmissionCurrent({
|
|
74
|
+
root: params.root,
|
|
75
|
+
filePath: params.filePath,
|
|
76
|
+
handle: params.opened.handle,
|
|
77
|
+
identity: params.identity,
|
|
78
|
+
symlinks: params.symlinks,
|
|
79
|
+
afterPathIdentityCheck: params.afterPathIdentityCheck,
|
|
80
|
+
});
|
|
81
|
+
return { ...params.opened, realPath: finalRealPath };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (params.identity.nlink <= 1n) {
|
|
85
|
+
await recordOpenedFileFailure(error, params.opened.handle, params.filePath, params.identity);
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await params.opened.handle.close().catch(() => { });
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
type ExactDirectoryIdentity = Readonly<Pick<BigIntStats, "dev" | "ino">>;
|
|
3
|
+
export type RemovalDirectoryAssertion = Readonly<{
|
|
4
|
+
path: string;
|
|
5
|
+
dev: bigint;
|
|
6
|
+
ino: bigint;
|
|
7
|
+
numericDev?: number;
|
|
8
|
+
numericIno?: number;
|
|
9
|
+
platform: NodeJS.Platform;
|
|
10
|
+
realPath?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function createRemovalDirectoryAssertion(path: string, identity: ExactDirectoryIdentity, realPath?: string, platform?: NodeJS.Platform): RemovalDirectoryAssertion;
|
|
13
|
+
export declare function assertRemovalDirectoryCurrent(assertion: RemovalDirectoryAssertion): void;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=root-remove-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-remove-identity.d.ts","sourceRoot":"","sources":["../src/root-remove-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,SAAS,CAAC;AAOlD,KAAK,sBAAsB,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;AAGzE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAaH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,yBAAyB,CAc3B;AA0DD,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,yBAAyB,GAAG,IAAI,CAUxF"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import { inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { realpathSync } from "./realpath.js";
|
|
5
|
+
import { directoryComponentNotDirectoryError } from "./root-errors.js";
|
|
6
|
+
const MAX_SAFE_DIRECTORY_IDENTITY = BigInt(Number.MAX_SAFE_INTEGER);
|
|
7
|
+
function identityMismatch(cause) {
|
|
8
|
+
return new FsSafeError("path-mismatch", "removal ancestor identity changed or could not be verified", {
|
|
9
|
+
cause: cause instanceof Error ? cause : undefined,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function safeIdentityNumber(value) {
|
|
13
|
+
if (value < 0n || value > MAX_SAFE_DIRECTORY_IDENTITY)
|
|
14
|
+
return undefined;
|
|
15
|
+
return Number(value);
|
|
16
|
+
}
|
|
17
|
+
export function createRemovalDirectoryAssertion(path, identity, realPath, platform = process.platform) {
|
|
18
|
+
const dev = safeIdentityNumber(identity.dev);
|
|
19
|
+
const ino = safeIdentityNumber(identity.ino);
|
|
20
|
+
const numeric = dev !== undefined && ino !== undefined &&
|
|
21
|
+
(platform !== "win32" || (dev !== 0 && ino !== 0));
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
path,
|
|
24
|
+
dev: identity.dev,
|
|
25
|
+
ino: identity.ino,
|
|
26
|
+
numericDev: numeric ? dev : undefined,
|
|
27
|
+
numericIno: numeric ? ino : undefined,
|
|
28
|
+
platform,
|
|
29
|
+
realPath,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function assertDirectoryType(stat) {
|
|
33
|
+
if (typeof stat?.isDirectory !== "function" || typeof stat.isSymbolicLink !== "function") {
|
|
34
|
+
throw identityMismatch();
|
|
35
|
+
}
|
|
36
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
37
|
+
throw directoryComponentNotDirectoryError();
|
|
38
|
+
}
|
|
39
|
+
function assertExactObservation(stat, expected, platform) {
|
|
40
|
+
assertDirectoryType(stat);
|
|
41
|
+
if (typeof stat.dev !== "bigint" || typeof stat.ino !== "bigint")
|
|
42
|
+
throw identityMismatch();
|
|
43
|
+
if (platform === "win32" && (stat.dev === 0n || stat.ino === 0n))
|
|
44
|
+
throw identityMismatch();
|
|
45
|
+
if (stat.dev !== expected.dev || stat.ino !== expected.ino)
|
|
46
|
+
throw identityMismatch();
|
|
47
|
+
}
|
|
48
|
+
function assertNumericObservation(assertion) {
|
|
49
|
+
const stat = fsSync.lstatSync(assertion.path);
|
|
50
|
+
assertDirectoryType(stat);
|
|
51
|
+
if (!Number.isSafeInteger(stat.dev) || stat.dev < 0 ||
|
|
52
|
+
!Number.isSafeInteger(stat.ino) || stat.ino < 0) {
|
|
53
|
+
throw identityMismatch();
|
|
54
|
+
}
|
|
55
|
+
let unknown = false;
|
|
56
|
+
const observedDev = stat.dev;
|
|
57
|
+
const expectedDev = assertion.numericDev;
|
|
58
|
+
if (assertion.platform === "win32" && observedDev === 0) {
|
|
59
|
+
unknown = true;
|
|
60
|
+
}
|
|
61
|
+
else if (observedDev !== expectedDev) {
|
|
62
|
+
// Reject a known mismatch before considering an unknown companion field.
|
|
63
|
+
throw identityMismatch();
|
|
64
|
+
}
|
|
65
|
+
const observedIno = stat.ino;
|
|
66
|
+
const expectedIno = assertion.numericIno;
|
|
67
|
+
if (assertion.platform === "win32" && observedIno === 0) {
|
|
68
|
+
unknown = true;
|
|
69
|
+
}
|
|
70
|
+
else if (observedIno !== expectedIno) {
|
|
71
|
+
throw identityMismatch();
|
|
72
|
+
}
|
|
73
|
+
if (unknown) {
|
|
74
|
+
// The numeric lstat is the first observation. Permit one exact retry only,
|
|
75
|
+
// retaining every known component and the original exact admission receipt.
|
|
76
|
+
assertExactObservation(fsSync.lstatSync(assertion.path, { bigint: true }), assertion, assertion.platform);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function assertRemovalDirectoryCurrent(assertion) {
|
|
80
|
+
if (assertion.numericDev !== undefined && assertion.numericIno !== undefined) {
|
|
81
|
+
assertNumericObservation(assertion);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
inspectDirectoryIdentitySync(assertion.path, assertion, undefined, assertion.platform);
|
|
85
|
+
}
|
|
86
|
+
if (assertion.realPath !== undefined && realpathSync.native(assertion.path) !== assertion.realPath) {
|
|
87
|
+
throw identityMismatch();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
export type CapturedRemovalDirectory = Readonly<{
|
|
3
|
+
path: string;
|
|
4
|
+
stat: BigIntStats;
|
|
5
|
+
}>;
|
|
6
|
+
export declare class RemovalPathReceipts {
|
|
7
|
+
private rootStat;
|
|
8
|
+
private directories;
|
|
9
|
+
readonly observeRoot: (stat: BigIntStats) => void;
|
|
10
|
+
observeDirectory(dir: string, stat: BigIntStats): void;
|
|
11
|
+
complete(rootReal: string, targetPath: string): {
|
|
12
|
+
rootStat: BigIntStats;
|
|
13
|
+
parent: CapturedRemovalDirectory | undefined;
|
|
14
|
+
directories: readonly CapturedRemovalDirectory[];
|
|
15
|
+
} | undefined;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=root-remove-receipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-remove-receipt.d.ts","sourceRoot":"","sources":["../src/root-remove-receipt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC,CAAC;AAKH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,WAAW,CAAoD;IAEvE,QAAQ,CAAC,WAAW,SAAU,WAAW,KAAG,IAAI,CAA8B;IAE9E,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAIrD;IAED,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG;QAC9C,QAAQ,EAAE,WAAW,CAAC;QACtB,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC;QAC7C,WAAW,EAAE,SAAS,wBAAwB,EAAE,CAAC;KAClD,GAAG,SAAS,CAiBZ;CACF"}
|