@openclaw/fs-safe 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +12 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +6 -2
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -19
- package/dist/archive-plan.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +18 -13
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -10
- package/dist/archive-tar-inspect.d.ts.map +1 -1
- package/dist/archive-tar-inspect.js +4 -1
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +3 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +13 -2
- package/dist/archive-zip-loader.d.ts +3 -2
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +30 -4
- package/dist/archive-zip-manifest.d.ts +5 -0
- package/dist/archive-zip-manifest.d.ts.map +1 -0
- package/dist/archive-zip-manifest.js +22 -0
- package/dist/archive-zip-names.d.ts +6 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +35 -14
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +3 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/darwin-acl.d.ts +4 -0
- package/dist/darwin-acl.d.ts.map +1 -0
- package/dist/darwin-acl.js +24 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +8 -2
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +67 -23
- package/dist/directory-entry-path.d.ts +3 -0
- package/dist/directory-entry-path.d.ts.map +1 -0
- package/dist/directory-entry-path.js +21 -0
- package/dist/directory-guard.d.ts +17 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +134 -48
- package/dist/directory-mode-node.d.ts +12 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +102 -4
- package/dist/effective-uid.d.ts +2 -0
- package/dist/effective-uid.d.ts.map +1 -0
- package/dist/effective-uid.js +25 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +98 -27
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +36 -6
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +29 -9
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +6 -2
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -65
- package/dist/file-store-copy-source.d.ts +5 -0
- package/dist/file-store-copy-source.d.ts.map +1 -0
- package/dist/file-store-copy-source.js +31 -0
- package/dist/file-store-path.d.ts.map +1 -1
- package/dist/file-store-path.js +4 -1
- package/dist/file-store-sync-directory.d.ts +16 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -0
- package/dist/file-store-sync-directory.js +349 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +11 -28
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +65 -20
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +8 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +176 -27
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +4 -1
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +54 -19
- package/dist/json-durable-queue-ownership.d.ts +6 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +89 -42
- package/dist/json-durable-queue-paths.d.ts +11 -0
- package/dist/json-durable-queue-paths.d.ts.map +1 -0
- package/dist/json-durable-queue-paths.js +42 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -1
- package/dist/json-durable-queue-read.js +2 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +50 -58
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +5 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +54 -22
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +4 -0
- package/dist/local-file-descriptor.d.ts +17 -0
- package/dist/local-file-descriptor.d.ts.map +1 -0
- package/dist/local-file-descriptor.js +84 -0
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -7
- package/dist/move-path-cleanup.d.ts +2 -0
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +44 -18
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +31 -6
- package/dist/native-binding.d.ts +14 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-directory-observation.d.ts +17 -0
- package/dist/native-directory-observation.d.ts.map +1 -0
- package/dist/native-directory-observation.js +37 -0
- package/dist/native-parent-admission.d.ts +31 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +124 -0
- package/dist/native-pinned-write-windows.d.ts +0 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +0 -3
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +309 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +15 -5
- package/dist/overwrite-file-handle.d.ts.map +1 -1
- package/dist/overwrite-file-handle.js +5 -1
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -0
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +7 -2
- package/dist/path-prefix.d.ts +7 -0
- package/dist/path-prefix.d.ts.map +1 -0
- package/dist/path-prefix.js +82 -0
- package/dist/path-scope-lexical.d.ts.map +1 -1
- package/dist/path-scope-lexical.js +18 -8
- package/dist/path-segment-route.d.ts +7 -0
- package/dist/path-segment-route.d.ts.map +1 -0
- package/dist/path-segment-route.js +24 -0
- package/dist/path-suffix-aliases.d.ts +10 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -0
- package/dist/path-suffix-aliases.js +386 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +16 -5
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +14 -3
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +37 -8
- package/dist/pinned-mutation-admission.d.ts +25 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -0
- package/dist/pinned-mutation-admission.js +425 -0
- package/dist/pinned-mutation-observation.d.ts +34 -0
- package/dist/pinned-mutation-observation.d.ts.map +1 -0
- package/dist/pinned-mutation-observation.js +142 -0
- package/dist/pinned-mutation-shared-route.d.ts +24 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
- package/dist/pinned-mutation-shared-route.js +70 -0
- package/dist/pinned-open.d.ts +6 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +28 -10
- package/dist/pinned-write-types.d.ts +75 -0
- package/dist/pinned-write-types.d.ts.map +1 -0
- package/dist/pinned-write-types.js +1 -0
- package/dist/pinned-write.d.ts +7 -33
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +151 -17
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +2 -0
- package/dist/private-producer-handoff.d.ts +16 -0
- package/dist/private-producer-handoff.d.ts.map +1 -0
- package/dist/private-producer-handoff.js +272 -0
- package/dist/private-temp-workspace.d.ts +2 -39
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +183 -77
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +33 -29
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +56 -45
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +12 -5
- package/dist/replace-file-temp-owner.d.ts +1 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +12 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +24 -24
- package/dist/root-boundary.d.ts +4 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +6 -1
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +57 -11
- package/dist/root-directory-creation.d.ts +13 -0
- package/dist/root-directory-creation.d.ts.map +1 -0
- package/dist/root-directory-creation.js +212 -0
- package/dist/root-directory-list.d.ts +16 -4
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +180 -39
- package/dist/root-directory.d.ts +23 -0
- package/dist/root-directory.d.ts.map +1 -0
- package/dist/root-directory.js +141 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +12 -1
- package/dist/root-file-final-admission.d.ts +21 -0
- package/dist/root-file-final-admission.d.ts.map +1 -0
- package/dist/root-file-final-admission.js +83 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +103 -24
- package/dist/root-impl.d.ts +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +289 -317
- package/dist/root-move-noreplace.d.ts +14 -0
- package/dist/root-move-noreplace.d.ts.map +1 -0
- package/dist/root-move-noreplace.js +202 -0
- package/dist/root-observed-path.d.ts +9 -0
- package/dist/root-observed-path.d.ts.map +1 -0
- package/dist/root-observed-path.js +95 -0
- package/dist/root-path-errors.d.ts +12 -0
- package/dist/root-path-errors.d.ts.map +1 -0
- package/dist/root-path-errors.js +13 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +53 -20
- package/dist/root-path-observation.d.ts +63 -0
- package/dist/root-path-observation.d.ts.map +1 -0
- package/dist/root-path-observation.js +180 -0
- package/dist/root-path-stat.d.ts +5 -0
- package/dist/root-path-stat.d.ts.map +1 -0
- package/dist/root-path-stat.js +101 -0
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +23 -4
- package/dist/root-path.d.ts +11 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +215 -53
- package/dist/root-paths-lexical.d.ts +9 -0
- package/dist/root-paths-lexical.d.ts.map +1 -0
- package/dist/root-paths-lexical.js +22 -0
- package/dist/root-paths.d.ts +3 -25
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +77 -154
- package/dist/root-read-admission.d.ts +26 -0
- package/dist/root-read-admission.d.ts.map +1 -0
- package/dist/root-read-admission.js +94 -0
- package/dist/root-remove-identity.d.ts +15 -0
- package/dist/root-remove-identity.d.ts.map +1 -0
- package/dist/root-remove-identity.js +89 -0
- package/dist/root-remove-receipt.d.ts +17 -0
- package/dist/root-remove-receipt.d.ts.map +1 -0
- package/dist/root-remove-receipt.js +37 -0
- package/dist/root-remove.d.ts +2 -1
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +172 -10
- package/dist/root-write-admission.d.ts +68 -0
- package/dist/root-write-admission.d.ts.map +1 -0
- package/dist/root-write-admission.js +322 -0
- package/dist/root-write-compatibility.d.ts +13 -0
- package/dist/root-write-compatibility.d.ts.map +1 -0
- package/dist/root-write-compatibility.js +74 -0
- package/dist/root-write-complete-parent.d.ts +42 -0
- package/dist/root-write-complete-parent.d.ts.map +1 -0
- package/dist/root-write-complete-parent.js +195 -0
- package/dist/root-write-publication.d.ts +24 -0
- package/dist/root-write-publication.d.ts.map +1 -0
- package/dist/root-write-publication.js +85 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -4
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +62 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +18 -13
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +5 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +92 -7
- package/dist/secure-temp-dir.d.ts +6 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +121 -101
- package/dist/secure-temp-repair.d.ts +35 -0
- package/dist/secure-temp-repair.d.ts.map +1 -0
- package/dist/secure-temp-repair.js +106 -0
- package/dist/sibling-staged-file.d.ts +3 -2
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +179 -55
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -13
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +44 -14
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/staged-directory.d.ts +14 -5
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +54 -3
- package/dist/standalone-publication-path.d.ts +2 -0
- package/dist/standalone-publication-path.d.ts.map +1 -0
- package/dist/standalone-publication-path.js +6 -0
- package/dist/stat-observation.d.ts +11 -0
- package/dist/stat-observation.d.ts.map +1 -0
- package/dist/stat-observation.js +64 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +39 -2
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +7 -2
- package/dist/temp-target.d.ts +2 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +130 -3
- package/dist/temp-workspace-admission.d.ts +22 -0
- package/dist/temp-workspace-admission.d.ts.map +1 -0
- package/dist/temp-workspace-admission.js +374 -0
- package/dist/temp-workspace-child-admission.d.ts +13 -0
- package/dist/temp-workspace-child-admission.d.ts.map +1 -0
- package/dist/temp-workspace-child-admission.js +95 -0
- package/dist/temp-workspace-descriptor.d.ts +48 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -0
- package/dist/temp-workspace-descriptor.js +363 -0
- package/dist/temp-workspace-identity.d.ts +15 -0
- package/dist/temp-workspace-identity.d.ts.map +1 -0
- package/dist/temp-workspace-identity.js +41 -0
- package/dist/temp-workspace-owner.d.ts +7 -6
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +121 -61
- package/dist/temp-workspace-permissions.d.ts +4 -0
- package/dist/temp-workspace-permissions.d.ts.map +1 -0
- package/dist/temp-workspace-permissions.js +32 -0
- package/dist/temp-workspace-types.d.ts +40 -0
- package/dist/temp-workspace-types.d.ts.map +1 -0
- package/dist/temp-workspace-types.js +1 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/temp.js +1 -1
- package/dist/test-hooks.d.ts +7 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -1
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +54 -8
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +5 -0
- package/dist/windows-path-alias.d.ts +39 -0
- package/dist/windows-path-alias.d.ts.map +1 -0
- package/dist/windows-path-alias.js +153 -0
- package/docs/advanced.md +25 -0
- package/docs/archive.md +23 -2
- package/docs/atomic.md +27 -3
- package/docs/copy.md +3 -0
- package/docs/durability.md +14 -0
- package/docs/errors.md +14 -6
- package/docs/file-store.md +24 -3
- package/docs/filename.md +14 -7
- package/docs/install-path.md +2 -2
- package/docs/install.md +8 -7
- package/docs/json-store.md +5 -1
- package/docs/json.md +11 -0
- package/docs/mutation-policy-proof.md +65 -0
- package/docs/native-helper.md +26 -9
- package/docs/native.md +32 -8
- package/docs/output.md +33 -11
- package/docs/path-prefix.md +64 -0
- package/docs/path-suffix-aliases.md +159 -0
- package/docs/path.md +11 -0
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +9 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +18 -3
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +6 -2
- package/docs/security-model.md +75 -8
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +5 -1
- package/docs/temp.md +260 -36
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
package/dist/root-paths.js
CHANGED
|
@@ -1,33 +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
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";
|
|
10
13
|
function invalidPath(scopeLabel) {
|
|
11
14
|
return {
|
|
12
15
|
ok: false,
|
|
13
16
|
error: `Invalid path: must stay within ${scopeLabel}`,
|
|
14
17
|
};
|
|
15
18
|
}
|
|
19
|
+
const INVALID_REAL_PATH = Symbol("invalid-real-path");
|
|
16
20
|
async function resolveRealPathIfExists(targetPath) {
|
|
21
|
+
if (hasWindowsPathAlias(targetPath, "filesystem"))
|
|
22
|
+
return INVALID_REAL_PATH;
|
|
17
23
|
try {
|
|
18
|
-
|
|
24
|
+
const realPath = realpathSync.native(pathForWindowsFilesystem(targetPath));
|
|
25
|
+
return hasWindowsPathAlias(realPath, "filesystem") ? INVALID_REAL_PATH : realPath;
|
|
19
26
|
}
|
|
20
27
|
catch {
|
|
21
28
|
return undefined;
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
async function resolveTrustedRootRealPath(rootDir) {
|
|
32
|
+
if (hasWindowsPathAlias(rootDir, "filesystem"))
|
|
33
|
+
return undefined;
|
|
25
34
|
try {
|
|
26
|
-
const
|
|
35
|
+
const operationPath = pathForWindowsFilesystem(rootDir);
|
|
36
|
+
const rootLstat = fsSync.lstatSync(operationPath);
|
|
27
37
|
if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) {
|
|
28
38
|
return undefined;
|
|
29
39
|
}
|
|
30
|
-
|
|
40
|
+
const realPath = realpathSync.native(operationPath);
|
|
41
|
+
return hasWindowsPathAlias(realPath, "filesystem") ? undefined : realPath;
|
|
31
42
|
}
|
|
32
43
|
catch {
|
|
33
44
|
return undefined;
|
|
@@ -35,7 +46,8 @@ async function resolveTrustedRootRealPath(rootDir) {
|
|
|
35
46
|
}
|
|
36
47
|
async function validateCanonicalPathWithinRoot(params) {
|
|
37
48
|
try {
|
|
38
|
-
const
|
|
49
|
+
const operationPath = pathForWindowsFilesystem(params.candidatePath);
|
|
50
|
+
const candidateLstat = fsSync.lstatSync(operationPath);
|
|
39
51
|
if (candidateLstat.isSymbolicLink()) {
|
|
40
52
|
return "invalid";
|
|
41
53
|
}
|
|
@@ -48,25 +60,33 @@ async function validateCanonicalPathWithinRoot(params) {
|
|
|
48
60
|
if (params.expect === "file" && candidateLstat.nlink > 1) {
|
|
49
61
|
return "invalid";
|
|
50
62
|
}
|
|
51
|
-
const candidateRealPath = realpathSync.native(
|
|
63
|
+
const candidateRealPath = realpathSync.native(operationPath);
|
|
64
|
+
if (hasWindowsPathAlias(candidateRealPath, "filesystem"))
|
|
65
|
+
return "invalid";
|
|
52
66
|
return isPathInside(params.rootRealPath, candidateRealPath) ? "ok" : "invalid";
|
|
53
67
|
}
|
|
54
68
|
catch (err) {
|
|
55
69
|
return isNotFoundPathError(err) ? "not-found" : "invalid";
|
|
56
70
|
}
|
|
57
71
|
}
|
|
58
|
-
export function resolvePathWithinRoot(params) {
|
|
59
|
-
return resolvePathWithinNormalizedRoot(params, path.resolve(params.rootDir));
|
|
60
|
-
}
|
|
61
72
|
export async function resolveWritablePathWithinRoot(params) {
|
|
62
|
-
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
|
+
});
|
|
63
83
|
if (!lexical.ok) {
|
|
64
84
|
return lexical;
|
|
65
85
|
}
|
|
66
|
-
const rootDir =
|
|
86
|
+
const rootDir = resolvePathPreservingWindowsRoot(rootDirInput);
|
|
67
87
|
const rootRealPath = await resolveTrustedRootRealPath(rootDir);
|
|
68
88
|
if (!rootRealPath) {
|
|
69
|
-
return invalidPath(
|
|
89
|
+
return invalidPath(scopeLabel);
|
|
70
90
|
}
|
|
71
91
|
const requestedPath = lexical.path;
|
|
72
92
|
const parentDir = path.dirname(requestedPath);
|
|
@@ -76,7 +96,7 @@ export async function resolveWritablePathWithinRoot(params) {
|
|
|
76
96
|
expect: "directory",
|
|
77
97
|
});
|
|
78
98
|
if (parentStatus !== "ok") {
|
|
79
|
-
return invalidPath(
|
|
99
|
+
return invalidPath(scopeLabel);
|
|
80
100
|
}
|
|
81
101
|
const targetStatus = await validateCanonicalPathWithinRoot({
|
|
82
102
|
rootRealPath,
|
|
@@ -84,131 +104,10 @@ export async function resolveWritablePathWithinRoot(params) {
|
|
|
84
104
|
expect: "file",
|
|
85
105
|
});
|
|
86
106
|
if (targetStatus === "invalid") {
|
|
87
|
-
return invalidPath(
|
|
107
|
+
return invalidPath(scopeLabel);
|
|
88
108
|
}
|
|
89
109
|
return lexical;
|
|
90
110
|
}
|
|
91
|
-
async function resolveNearestExistingPath(targetPath) {
|
|
92
|
-
let current = path.resolve(targetPath);
|
|
93
|
-
while (true) {
|
|
94
|
-
try {
|
|
95
|
-
fsSync.lstatSync(current);
|
|
96
|
-
return current;
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
if (!isNotFoundPathError(err)) {
|
|
100
|
-
throw err;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const parent = path.dirname(current);
|
|
104
|
-
if (parent === current) {
|
|
105
|
-
throw new Error(`failed to resolve existing path for ${targetPath}`);
|
|
106
|
-
}
|
|
107
|
-
current = parent;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async function assertNoSymlinkSegments(params) {
|
|
111
|
-
const relative = path.relative(params.rootDir, params.targetPath);
|
|
112
|
-
if (isPathRelativeEscape(relative)) {
|
|
113
|
-
throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
|
|
114
|
-
}
|
|
115
|
-
let current = params.rootDir;
|
|
116
|
-
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
117
|
-
current = path.join(current, segment);
|
|
118
|
-
try {
|
|
119
|
-
const stat = fsSync.lstatSync(current);
|
|
120
|
-
if (stat.isSymbolicLink()) {
|
|
121
|
-
throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
|
|
122
|
-
}
|
|
123
|
-
if (!stat.isDirectory()) {
|
|
124
|
-
throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
if (isNotFoundPathError(err)) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
throw err;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
export async function ensureDirectoryWithinRoot(params) {
|
|
136
|
-
const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
|
|
137
|
-
try {
|
|
138
|
-
assertNoNulPathInput(params.rootDir);
|
|
139
|
-
assertNoNulPathInput(params.requestedPath);
|
|
140
|
-
if (!params.requestedPath.trim())
|
|
141
|
-
assertNoNulPathInput(params.defaultDirName ?? "");
|
|
142
|
-
const lexical = resolvePathWithinRoot({
|
|
143
|
-
...params, scopeLabel, defaultFileName: params.defaultDirName,
|
|
144
|
-
});
|
|
145
|
-
if (!lexical.ok)
|
|
146
|
-
return lexical;
|
|
147
|
-
const rootDir = path.resolve(params.rootDir);
|
|
148
|
-
const targetPath = lexical.path;
|
|
149
|
-
const rootStat = fsSync.lstatSync(rootDir);
|
|
150
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
151
|
-
return invalidPath(scopeLabel);
|
|
152
|
-
}
|
|
153
|
-
await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
|
|
154
|
-
const rootReal = realpathSync.native(rootDir);
|
|
155
|
-
const nearestExistingPath = await resolveNearestExistingPath(targetPath);
|
|
156
|
-
const nearestExistingReal = realpathSync.native(nearestExistingPath);
|
|
157
|
-
if (!isPathInside(rootReal, nearestExistingReal)) {
|
|
158
|
-
return invalidPath(scopeLabel);
|
|
159
|
-
}
|
|
160
|
-
const relative = path.relative(rootDir, targetPath);
|
|
161
|
-
let current = rootDir;
|
|
162
|
-
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
163
|
-
current = path.join(current, segment);
|
|
164
|
-
while (true) {
|
|
165
|
-
try {
|
|
166
|
-
const stat = fsSync.lstatSync(current);
|
|
167
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
168
|
-
return invalidPath(scopeLabel);
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
if (!isNotFoundPathError(err)) {
|
|
174
|
-
throw err;
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
await fs.mkdir(current, { mode: params.mode });
|
|
178
|
-
}
|
|
179
|
-
catch (mkdirErr) {
|
|
180
|
-
if (hasNodeErrorCode(mkdirErr, "EEXIST")) {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
throw mkdirErr;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const currentReal = realpathSync.native(current);
|
|
188
|
-
if (!isPathInside(rootReal, currentReal)) {
|
|
189
|
-
return invalidPath(scopeLabel);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const targetReal = realpathSync.native(targetPath);
|
|
193
|
-
if (!isPathInside(rootReal, targetReal)) {
|
|
194
|
-
return invalidPath(scopeLabel);
|
|
195
|
-
}
|
|
196
|
-
return { ok: true, path: targetPath };
|
|
197
|
-
}
|
|
198
|
-
catch (cause) {
|
|
199
|
-
if ((cause instanceof FsSafeError && cause.category === "policy") ||
|
|
200
|
-
hasNodeErrorCode(cause, "ENOTDIR")) {
|
|
201
|
-
return invalidPath(scopeLabel);
|
|
202
|
-
}
|
|
203
|
-
// Native messages include unbounded paths; display only bounded errno metadata.
|
|
204
|
-
const code = isNodeError(cause) && typeof cause.code === "string"
|
|
205
|
-
? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
|
|
206
|
-
const syscall = isNodeError(cause) && typeof cause.syscall === "string"
|
|
207
|
-
? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
|
|
208
|
-
const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
|
|
209
|
-
return { ok: false, error: diagnostic.message, diagnostic };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
111
|
export function resolvePathsWithinRoot(params) {
|
|
213
112
|
const resolvedPaths = [];
|
|
214
113
|
let previousRootDir;
|
|
@@ -219,11 +118,16 @@ export function resolvePathsWithinRoot(params) {
|
|
|
219
118
|
requestedPath: raw,
|
|
220
119
|
scopeLabel: params.scopeLabel,
|
|
221
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);
|
|
222
126
|
// Relative roots may follow cwd changes made by an input iterator or getter.
|
|
223
127
|
const resolvedRoot = process.platform !== "win32" && typeof request.rootDir === "string" &&
|
|
224
128
|
request.rootDir.startsWith("/") && request.rootDir === previousRootDir
|
|
225
129
|
? previousResolvedRoot
|
|
226
|
-
:
|
|
130
|
+
: resolvePathPreservingWindowsRoot(request.rootDir);
|
|
227
131
|
previousRootDir = request.rootDir;
|
|
228
132
|
previousResolvedRoot = resolvedRoot;
|
|
229
133
|
const pathResult = resolvePathWithinNormalizedRoot(request, resolvedRoot);
|
|
@@ -241,10 +145,11 @@ export async function resolveStrictExistingPathsWithinRoot(params) {
|
|
|
241
145
|
return await resolveCheckedPathsWithinRoot(params, false);
|
|
242
146
|
}
|
|
243
147
|
export function pathScope(rootDir, options) {
|
|
244
|
-
const
|
|
148
|
+
const label = options.label;
|
|
149
|
+
const base = { rootDir, scopeLabel: label };
|
|
245
150
|
return {
|
|
246
151
|
rootDir,
|
|
247
|
-
label
|
|
152
|
+
label,
|
|
248
153
|
resolve: (requestedPath, pathOptions) => resolvePathWithinRoot({
|
|
249
154
|
...base,
|
|
250
155
|
requestedPath,
|
|
@@ -276,8 +181,19 @@ export function pathScope(rootDir, options) {
|
|
|
276
181
|
};
|
|
277
182
|
}
|
|
278
183
|
async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
279
|
-
const
|
|
280
|
-
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;
|
|
281
197
|
const root = rootRealPath ? await openRoot(rootDir) : undefined;
|
|
282
198
|
const isInRoot = (relativePath) => Boolean(relativePath) &&
|
|
283
199
|
relativePath !== ".." &&
|
|
@@ -288,7 +204,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
288
204
|
const lexicalPathResult = resolvePathWithinRoot({
|
|
289
205
|
rootDir,
|
|
290
206
|
requestedPath,
|
|
291
|
-
scopeLabel
|
|
207
|
+
scopeLabel,
|
|
292
208
|
});
|
|
293
209
|
if (lexicalPathResult.ok) {
|
|
294
210
|
return {
|
|
@@ -297,11 +213,17 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
297
213
|
fallbackPath: lexicalPathResult.path,
|
|
298
214
|
};
|
|
299
215
|
}
|
|
300
|
-
if (!rootRealPath ||
|
|
216
|
+
if (!rootRealPath ||
|
|
217
|
+
!raw ||
|
|
218
|
+
!path.isAbsolute(raw) ||
|
|
219
|
+
hasWindowsPathAlias(raw, "filesystem")) {
|
|
301
220
|
return lexicalPathResult;
|
|
302
221
|
}
|
|
303
222
|
try {
|
|
304
|
-
const resolvedExistingPath = realpathSync.native(raw);
|
|
223
|
+
const resolvedExistingPath = realpathSync.native(pathForWindowsFilesystem(raw));
|
|
224
|
+
if (hasWindowsPathAlias(resolvedExistingPath, "filesystem")) {
|
|
225
|
+
return lexicalPathResult;
|
|
226
|
+
}
|
|
305
227
|
const relativePath = path.relative(rootRealPath, resolvedExistingPath);
|
|
306
228
|
if (!isInRoot(relativePath)) {
|
|
307
229
|
return lexicalPathResult;
|
|
@@ -317,7 +239,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
317
239
|
}
|
|
318
240
|
};
|
|
319
241
|
const resolvedPaths = [];
|
|
320
|
-
for (const raw of
|
|
242
|
+
for (const raw of requestedPaths) {
|
|
321
243
|
const pathResult = await resolveExistingRelativePath(raw);
|
|
322
244
|
if (!pathResult.ok) {
|
|
323
245
|
return { ok: false, error: pathResult.error };
|
|
@@ -340,16 +262,17 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
340
262
|
await assertNoSymlinkSegments({
|
|
341
263
|
rootDir,
|
|
342
264
|
targetPath: pathResult.fallbackPath,
|
|
343
|
-
scopeLabel
|
|
265
|
+
scopeLabel,
|
|
344
266
|
});
|
|
345
267
|
const existingPath = await resolveNearestExistingPath(pathResult.fallbackPath);
|
|
346
|
-
const existingRealPath = realpathSync.native(existingPath);
|
|
347
|
-
if (
|
|
348
|
-
|
|
268
|
+
const existingRealPath = realpathSync.native(pathForWindowsFilesystem(existingPath));
|
|
269
|
+
if (hasWindowsPathAlias(existingRealPath, "filesystem") ||
|
|
270
|
+
!isPathInside(rootRealPath, existingRealPath)) {
|
|
271
|
+
return invalidPath(scopeLabel);
|
|
349
272
|
}
|
|
350
273
|
}
|
|
351
274
|
catch {
|
|
352
|
-
return invalidPath(
|
|
275
|
+
return invalidPath(scopeLabel);
|
|
353
276
|
}
|
|
354
277
|
resolvedPaths.push(pathResult.fallbackPath);
|
|
355
278
|
continue;
|
|
@@ -357,12 +280,12 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
357
280
|
if (err instanceof FsSafeError && err.code === "outside-workspace") {
|
|
358
281
|
return {
|
|
359
282
|
ok: false,
|
|
360
|
-
error: `File is outside ${
|
|
283
|
+
error: `File is outside ${scopeLabel}`,
|
|
361
284
|
};
|
|
362
285
|
}
|
|
363
286
|
return {
|
|
364
287
|
ok: false,
|
|
365
|
-
error: `Invalid path: must stay within ${
|
|
288
|
+
error: `Invalid path: must stay within ${scopeLabel} and be a regular non-symlink file`,
|
|
366
289
|
};
|
|
367
290
|
}
|
|
368
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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { isPathInside } from "./path.js";
|
|
3
|
+
// Private to a single nonrecursive removal. No observations are shared across
|
|
4
|
+
// operations, and a lexical observation never stands in for an unvisited alias
|
|
5
|
+
// target on the final canonical chain.
|
|
6
|
+
export class RemovalPathReceipts {
|
|
7
|
+
rootStat;
|
|
8
|
+
directories;
|
|
9
|
+
observeRoot = (stat) => { this.rootStat = stat; };
|
|
10
|
+
observeDirectory(dir, stat) {
|
|
11
|
+
// Retain the first object if raw traversal revisits the same spelling.
|
|
12
|
+
const directories = this.directories ??= new Map();
|
|
13
|
+
if (!directories.has(dir))
|
|
14
|
+
directories.set(dir, { path: dir, stat });
|
|
15
|
+
}
|
|
16
|
+
complete(rootReal, targetPath) {
|
|
17
|
+
const parentPath = path.dirname(targetPath);
|
|
18
|
+
if (!this.rootStat || !isPathInside(rootReal, parentPath))
|
|
19
|
+
return undefined;
|
|
20
|
+
const directories = [];
|
|
21
|
+
let current = parentPath;
|
|
22
|
+
while (current !== rootReal) {
|
|
23
|
+
// Exact spelling is intentional: case/namespace aliases and symlink hops
|
|
24
|
+
// that did not visit the complete canonical chain use fresh admission.
|
|
25
|
+
const directory = this.directories?.get(current);
|
|
26
|
+
if (!directory)
|
|
27
|
+
return undefined;
|
|
28
|
+
directories.push(directory);
|
|
29
|
+
const parent = path.dirname(current);
|
|
30
|
+
if (parent === current)
|
|
31
|
+
return undefined;
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
directories.reverse();
|
|
35
|
+
return { rootStat: this.rootStat, parent: directories.at(-1), directories };
|
|
36
|
+
}
|
|
37
|
+
}
|