@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/walk.js
CHANGED
|
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { realpathSync } from "./realpath.js";
|
|
5
|
+
import { pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
5
6
|
function validateWalkBudget(name, value) {
|
|
6
7
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
7
8
|
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
@@ -69,7 +70,7 @@ function resolveKind(fullPath, dirent, symlinks) {
|
|
|
69
70
|
}
|
|
70
71
|
export function walkDirectorySync(rootDir, options = {}) {
|
|
71
72
|
validateWalkOptions(options);
|
|
72
|
-
const root =
|
|
73
|
+
const root = resolvePathPreservingWindowsRoot(rootDir);
|
|
73
74
|
const symlinks = options.symlinks ?? "skip";
|
|
74
75
|
const result = {
|
|
75
76
|
entries: [],
|
|
@@ -82,8 +83,9 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
82
83
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
83
84
|
return;
|
|
84
85
|
let realDir;
|
|
86
|
+
const operationPath = pathForWindowsFilesystem(dir);
|
|
85
87
|
try {
|
|
86
|
-
realDir = realpathSync(
|
|
88
|
+
realDir = realpathSync(operationPath);
|
|
87
89
|
}
|
|
88
90
|
catch (error) {
|
|
89
91
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -94,7 +96,7 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
94
96
|
visitedDirs.add(realDir);
|
|
95
97
|
let entries;
|
|
96
98
|
try {
|
|
97
|
-
entries = fsSync.readdirSync(
|
|
99
|
+
entries = fsSync.readdirSync(operationPath, { withFileTypes: true });
|
|
98
100
|
}
|
|
99
101
|
catch (error) {
|
|
100
102
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -129,7 +131,7 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
129
131
|
}
|
|
130
132
|
export async function walkDirectory(rootDir, options = {}) {
|
|
131
133
|
validateWalkOptions(options);
|
|
132
|
-
const root =
|
|
134
|
+
const root = resolvePathPreservingWindowsRoot(rootDir);
|
|
133
135
|
const symlinks = options.symlinks ?? "skip";
|
|
134
136
|
const result = {
|
|
135
137
|
entries: [],
|
|
@@ -142,8 +144,9 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
142
144
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
143
145
|
return;
|
|
144
146
|
let realDir;
|
|
147
|
+
const operationPath = pathForWindowsFilesystem(dir);
|
|
145
148
|
try {
|
|
146
|
-
realDir = realpathSync.native(
|
|
149
|
+
realDir = realpathSync.native(operationPath);
|
|
147
150
|
}
|
|
148
151
|
catch (error) {
|
|
149
152
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -154,7 +157,7 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
154
157
|
visitedDirs.add(realDir);
|
|
155
158
|
let entries;
|
|
156
159
|
try {
|
|
157
|
-
entries = await fs.readdir(
|
|
160
|
+
entries = await fs.readdir(operationPath, { withFileTypes: true });
|
|
158
161
|
}
|
|
159
162
|
catch (error) {
|
|
160
163
|
recordFailedDir(result, root, dir, depth, error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAI9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmD/B"}
|
package/dist/windows-owner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
|
|
2
2
|
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
3
|
+
import { hasWindowsPathAlias } from "./windows-path-alias.js";
|
|
3
4
|
const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
|
|
4
5
|
const TRUSTED_OWNER_SIDS = new Set(["s-1-5-18", "s-1-5-32-544"]);
|
|
5
6
|
function normalizeSid(value) {
|
|
@@ -47,6 +48,10 @@ function parseWindowsAclFacts(parsed) {
|
|
|
47
48
|
return { daclPresent: parsed.daclPresent, aces };
|
|
48
49
|
}
|
|
49
50
|
export async function inspectWindowsOwner(params) {
|
|
51
|
+
if (hasWindowsPathAlias(params.targetPath, "filesystem", "win32")) {
|
|
52
|
+
const error = new Error("Path uses a Windows filesystem namespace alias");
|
|
53
|
+
return { error: String(error), errorCause: error };
|
|
54
|
+
}
|
|
50
55
|
let command = "";
|
|
51
56
|
let startedAt = performance.now();
|
|
52
57
|
try {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export type WindowsPathAliasKind = "filesystem" | "relative";
|
|
3
|
+
/**
|
|
4
|
+
* Capture an ordinary Windows drive-relative path without normalizing its raw
|
|
5
|
+
* suffix. This is only for public APIs whose existing contract accepts such
|
|
6
|
+
* paths; callers must still run namespace-alias admission on the result.
|
|
7
|
+
*/
|
|
8
|
+
export declare function anchorWindowsDriveRelativePath(value: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a path without letting Node erase the separator from an exact
|
|
11
|
+
* extended-length drive root such as `\\?\C:\`. Bare `\\?\C:` input remains
|
|
12
|
+
* unchanged so the surrounding alias admission rejects it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolvePathPreservingWindowsRoot(value: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Preserve a namespaced drive root after a caller has already resolved the
|
|
17
|
+
* input. This lets admission fast paths keep exactly one live path.resolve
|
|
18
|
+
* call while retaining the same root-repair behavior as the general helper.
|
|
19
|
+
*/
|
|
20
|
+
export declare function repairResolvedWindowsRoot(value: string, resolved: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve path segments against a base while preserving a namespaced drive
|
|
23
|
+
* root when Node normalizes a legitimate rooted input back to that root.
|
|
24
|
+
* Raw bare namespace drives stay bare so admission checks still reject them.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolvePathFromBasePreservingWindowsRoot(base: string, ...segments: string[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* Adapt an admitted namespaced drive root for Node's Windows filesystem layer.
|
|
29
|
+
* Node removes the root separator from these paths during filesystem dispatch,
|
|
30
|
+
* so use the equivalent ordinary drive root for the operation. This is not an
|
|
31
|
+
* admission check: callers must validate attacker-controlled input first.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pathForWindowsFilesystem(value: string): string;
|
|
34
|
+
/** Returns true when a Windows pathname can address an alternate filesystem namespace. */
|
|
35
|
+
export declare function hasWindowsPathAlias(value: string, kind: WindowsPathAliasKind, platform?: NodeJS.Platform | string): boolean;
|
|
36
|
+
export declare function assertNoWindowsPathAliasForPlatform(value: string, kind: WindowsPathAliasKind, message: string, platform: NodeJS.Platform | string | undefined): void;
|
|
37
|
+
export declare function assertNoWindowsPathAlias(value: string, kind?: WindowsPathAliasKind, message?: string, platform?: NodeJS.Platform | string): void;
|
|
38
|
+
export declare function isWindowsPathAliasError(error: unknown): error is FsSafeError;
|
|
39
|
+
//# sourceMappingURL=windows-path-alias.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-path-alias.d.ts","sourceRoot":"","sources":["../src/windows-path-alias.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,UAAU,CAAC;AAsD7D;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYpE;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUjF;AAED;;;;GAIG;AACH,wBAAgB,wCAAwC,CACtD,IAAI,EAAE,MAAM,EACZ,GAAG,QAAQ,EAAE,MAAM,EAAE,GACpB,MAAM,CAgBR;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkB9D;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,oBAAoB,EAC1B,QAAQ,GAAE,MAAM,CAAC,QAAQ,GAAG,MAAyB,GACpD,OAAO,CAMT;AAED,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,oBAAoB,EAC1B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,GAAG,SAAS,GAC7C,IAAI,CAMN;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,oBAAmC,EACzC,OAAO,SAAmD,EAC1D,QAAQ,GAAE,MAAM,CAAC,QAAQ,GAAG,MAAyB,GACpD,IAAI,CAMN;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAE5E"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
const COLON = 0x3a;
|
|
4
|
+
const FORWARD_SLASH = 0x2f;
|
|
5
|
+
const BACKSLASH = 0x5c;
|
|
6
|
+
const DOT = 0x2e;
|
|
7
|
+
const QUESTION_MARK = 0x3f;
|
|
8
|
+
function isAsciiLetter(code) {
|
|
9
|
+
return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a);
|
|
10
|
+
}
|
|
11
|
+
function isSeparator(code) {
|
|
12
|
+
return code === FORWARD_SLASH || code === BACKSLASH;
|
|
13
|
+
}
|
|
14
|
+
function rootedDriveColonIndex(value) {
|
|
15
|
+
if (value.length >= 3 &&
|
|
16
|
+
isAsciiLetter(value.charCodeAt(0)) &&
|
|
17
|
+
value.charCodeAt(1) === COLON &&
|
|
18
|
+
isSeparator(value.charCodeAt(2))) {
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (value.length >= 7 &&
|
|
22
|
+
isSeparator(value.charCodeAt(0)) &&
|
|
23
|
+
isSeparator(value.charCodeAt(1)) &&
|
|
24
|
+
(value.charCodeAt(2) === QUESTION_MARK || value.charCodeAt(2) === DOT) &&
|
|
25
|
+
isSeparator(value.charCodeAt(3)) &&
|
|
26
|
+
isAsciiLetter(value.charCodeAt(4)) &&
|
|
27
|
+
value.charCodeAt(5) === COLON &&
|
|
28
|
+
isSeparator(value.charCodeAt(6))) {
|
|
29
|
+
return 5;
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
function isBareWindowsNamespaceDrive(value) {
|
|
34
|
+
return (value.length === 6 &&
|
|
35
|
+
isSeparator(value.charCodeAt(0)) &&
|
|
36
|
+
isSeparator(value.charCodeAt(1)) &&
|
|
37
|
+
(value.charCodeAt(2) === QUESTION_MARK || value.charCodeAt(2) === DOT) &&
|
|
38
|
+
isSeparator(value.charCodeAt(3)) &&
|
|
39
|
+
isAsciiLetter(value.charCodeAt(4)) &&
|
|
40
|
+
value.charCodeAt(5) === COLON);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Capture an ordinary Windows drive-relative path without normalizing its raw
|
|
44
|
+
* suffix. This is only for public APIs whose existing contract accepts such
|
|
45
|
+
* paths; callers must still run namespace-alias admission on the result.
|
|
46
|
+
*/
|
|
47
|
+
export function anchorWindowsDriveRelativePath(value) {
|
|
48
|
+
if (process.platform !== "win32" || path.isAbsolute(value))
|
|
49
|
+
return value;
|
|
50
|
+
if (value.length < 2 ||
|
|
51
|
+
!isAsciiLetter(value.charCodeAt(0)) ||
|
|
52
|
+
value.charCodeAt(1) !== COLON) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
const drive = value.slice(0, 2);
|
|
56
|
+
const base = path.resolve(drive);
|
|
57
|
+
return `${base}${path.sep}${value.slice(2)}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a path without letting Node erase the separator from an exact
|
|
61
|
+
* extended-length drive root such as `\\?\C:\`. Bare `\\?\C:` input remains
|
|
62
|
+
* unchanged so the surrounding alias admission rejects it.
|
|
63
|
+
*/
|
|
64
|
+
export function resolvePathPreservingWindowsRoot(value) {
|
|
65
|
+
if (value.length === 7 &&
|
|
66
|
+
process.platform === "win32" &&
|
|
67
|
+
rootedDriveColonIndex(value) === 5) {
|
|
68
|
+
return value.includes("/") ? value.replaceAll("/", "\\") : value;
|
|
69
|
+
}
|
|
70
|
+
const resolved = path.resolve(value);
|
|
71
|
+
return repairResolvedWindowsRoot(value, resolved);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Preserve a namespaced drive root after a caller has already resolved the
|
|
75
|
+
* input. This lets admission fast paths keep exactly one live path.resolve
|
|
76
|
+
* call while retaining the same root-repair behavior as the general helper.
|
|
77
|
+
*/
|
|
78
|
+
export function repairResolvedWindowsRoot(value, resolved) {
|
|
79
|
+
if (resolved.length === 6 &&
|
|
80
|
+
process.platform === "win32" &&
|
|
81
|
+
isBareWindowsNamespaceDrive(resolved) &&
|
|
82
|
+
!hasWindowsPathAlias(value, "filesystem")) {
|
|
83
|
+
return `${resolved}\\`;
|
|
84
|
+
}
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve path segments against a base while preserving a namespaced drive
|
|
89
|
+
* root when Node normalizes a legitimate rooted input back to that root.
|
|
90
|
+
* Raw bare namespace drives stay bare so admission checks still reject them.
|
|
91
|
+
*/
|
|
92
|
+
export function resolvePathFromBasePreservingWindowsRoot(base, ...segments) {
|
|
93
|
+
const resolved = path.resolve(base, ...segments);
|
|
94
|
+
if (resolved.length !== 6 ||
|
|
95
|
+
process.platform !== "win32" ||
|
|
96
|
+
!isBareWindowsNamespaceDrive(resolved)) {
|
|
97
|
+
return resolved;
|
|
98
|
+
}
|
|
99
|
+
if (hasWindowsPathAlias(base, "filesystem") ||
|
|
100
|
+
segments.some((segment) => hasWindowsPathAlias(segment, "filesystem"))) {
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
return `${resolved}\\`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Adapt an admitted namespaced drive root for Node's Windows filesystem layer.
|
|
107
|
+
* Node removes the root separator from these paths during filesystem dispatch,
|
|
108
|
+
* so use the equivalent ordinary drive root for the operation. This is not an
|
|
109
|
+
* admission check: callers must validate attacker-controlled input first.
|
|
110
|
+
*/
|
|
111
|
+
export function pathForWindowsFilesystem(value) {
|
|
112
|
+
if (process.platform !== "win32" ||
|
|
113
|
+
rootedDriveColonIndex(value) !== 5) {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
if (value.length === 7) {
|
|
117
|
+
return `${value[4]}:\\`;
|
|
118
|
+
}
|
|
119
|
+
const resolved = path.resolve(value);
|
|
120
|
+
if (isBareWindowsNamespaceDrive(resolved) &&
|
|
121
|
+
!hasWindowsPathAlias(value, "filesystem")) {
|
|
122
|
+
return `${resolved[4]}:\\`;
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
/** Returns true when a Windows pathname can address an alternate filesystem namespace. */
|
|
127
|
+
export function hasWindowsPathAlias(value, kind, platform = process.platform) {
|
|
128
|
+
if (platform !== "win32")
|
|
129
|
+
return false;
|
|
130
|
+
const firstColon = value.indexOf(":");
|
|
131
|
+
if (firstColon === -1)
|
|
132
|
+
return false;
|
|
133
|
+
if (kind === "relative")
|
|
134
|
+
return true;
|
|
135
|
+
return firstColon !== rootedDriveColonIndex(value) || value.indexOf(":", firstColon + 1) !== -1;
|
|
136
|
+
}
|
|
137
|
+
export function assertNoWindowsPathAliasForPlatform(value, kind, message, platform) {
|
|
138
|
+
if (hasWindowsPathAlias(value, kind, platform)) {
|
|
139
|
+
throw new FsSafeError("invalid-path", message, {
|
|
140
|
+
details: { reason: "windows-path-alias" },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function assertNoWindowsPathAlias(value, kind = "filesystem", message = "path uses a Windows filesystem namespace alias", platform = process.platform) {
|
|
145
|
+
if (hasWindowsPathAlias(value, kind, platform)) {
|
|
146
|
+
throw new FsSafeError("invalid-path", message, {
|
|
147
|
+
details: { reason: "windows-path-alias" },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function isWindowsPathAliasError(error) {
|
|
152
|
+
return error instanceof FsSafeError && error.details?.reason === "windows-path-alias";
|
|
153
|
+
}
|
package/docs/advanced.md
CHANGED
|
@@ -32,7 +32,9 @@ The exports group into a handful of themes. Each documented helper has its own p
|
|
|
32
32
|
| `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
|
|
33
33
|
| `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
|
|
34
34
|
| `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
|
|
35
|
+
| `resolvePathPrefixSync`, `ResolvedPathPrefix` | [path-prefix.md](path-prefix.md) | Follow physical symlink targets and return a canonical existing prefix with the raw missing suffix; propagate uncertain resolution failures. |
|
|
35
36
|
| `probePathCaseInsensitiveSync`, `ProbePathCaseOptions` | [path-case.md](path-case.md) | Observe local ASCII-case behavior with explicit read-only mode and owned temporary-probe cleanup. |
|
|
37
|
+
| `probePathSuffixAliasesSync`, `ProbePathSuffixAliasesOptions` | [path-suffix-aliases.md](path-suffix-aliases.md) | Observe selected missing suffix aliases with bounded temporary directory probes; ambiguity, dynamic budget exhaustion, or incomplete cleanup returns `undefined`. |
|
|
36
38
|
|
|
37
39
|
`ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
|
|
38
40
|
returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
|
|
@@ -77,6 +79,11 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
77
79
|
| `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
|
|
78
80
|
| `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
|
|
79
81
|
|
|
82
|
+
`pathExists()` and `pathExistsSync()` intentionally retain ordinary `stat`
|
|
83
|
+
semantics and are not caller-path admission boundaries. Validate an untrusted
|
|
84
|
+
path with the boundary appropriate to the operation before using these
|
|
85
|
+
existence probes.
|
|
86
|
+
|
|
80
87
|
`openRootFile()` and `openRootFileSync()` compare exact bigint identities before
|
|
81
88
|
open, on the retained descriptor, and on the current resolved path. Their `stat`
|
|
82
89
|
receipts remain numeric. Custom `ioFs` adapters must honor `{ bigint: true }` for
|
|
@@ -84,6 +91,24 @@ receipts remain numeric. Custom `ioFs` adapters must honor `{ bigint: true }` fo
|
|
|
84
91
|
Windows identities receive one re-inspection without reopening, then fail
|
|
85
92
|
validation if still unknown.
|
|
86
93
|
|
|
94
|
+
On Windows, these existing-object readers retain their historical support for a
|
|
95
|
+
leading drive-relative spelling such as `C:existing.txt`: it is anchored to that
|
|
96
|
+
drive before confinement and namespace admission. Additional colons remain in
|
|
97
|
+
the anchored spelling, so alternate-stream and directory-index aliases are still
|
|
98
|
+
rejected before opening.
|
|
99
|
+
|
|
100
|
+
These adapters also capture the canonical root directory's exact bigint identity
|
|
101
|
+
before component traversal. Immediately before transferring descriptor ownership,
|
|
102
|
+
they check that root, freshly canonicalize the consumed pathname, admit the fresh
|
|
103
|
+
spelling under the captured root, compare a no-follow canonical-leaf observation
|
|
104
|
+
with the retained descriptor, and check the root again. Boundary or identity drift
|
|
105
|
+
is a validation failure and the descriptor is closed. A custom `ioFs` supplies
|
|
106
|
+
these observations; the built-in adapter uses fs-safe's native realpath wrapper.
|
|
107
|
+
On Windows, the built-in adapter binds native root spelling before traversal,
|
|
108
|
+
including supplied `rootRealPath`, and returns that spelling in its root receipt.
|
|
109
|
+
This is an operation-local detection fence, not atomic confinement against a peer
|
|
110
|
+
that can keep racing pathname bindings.
|
|
111
|
+
|
|
87
112
|
The explicit `symlinks` policy takes precedence over the existing `rejectSymlinks`
|
|
88
113
|
boolean. Without `symlinks`, `rejectSymlinks: false` retains its existing behavior
|
|
89
114
|
of following contained links, and omission still rejects all symlink components.
|
package/docs/archive.md
CHANGED
|
@@ -139,7 +139,23 @@ accepted-entry plan back to Rust. Rust owns raw-stream admission, decompression,
|
|
|
139
139
|
and fd-relative `mkdirBeneath`/exclusive-open writes. This keeps filter policy identical
|
|
140
140
|
between native and JavaScript paths rather than reimplementing it in Rust.
|
|
141
141
|
|
|
142
|
-
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation
|
|
142
|
+
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless internal separator and dot-component equivalence is allowed only after validation; every raw and Unicode interpretation must also agree on whether its name ends in `/` or `\`. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
143
|
+
|
|
144
|
+
ZIP admission establishes each entry's kind before callbacks: a high-word UNIX
|
|
145
|
+
symlink type takes precedence regardless of creator, followed by the DOS directory
|
|
146
|
+
bit, the exact UNIX directory type, or a terminal slash/backslash. Native manifests
|
|
147
|
+
must agree with this kind, physical index, size, known path, and UNIX-creator mode
|
|
148
|
+
before extraction or any member read. Bounded ZIP reads retain this metadata from
|
|
149
|
+
their single admission pass without another input copy or scan.
|
|
150
|
+
|
|
151
|
+
Portable ZIP preflight, extraction, and reads also check the decoded kind against
|
|
152
|
+
admission. Unsupported JSZip metadata rejects with `ArchiveFormatError` before
|
|
153
|
+
filters, including UNIX-only directory attributes without a terminal slash or DOS
|
|
154
|
+
directory bit, backslash-only directory names without directory attributes, and
|
|
155
|
+
non-UNIX creators whose high-word symlink mode JSZip does not expose. Symlinks that
|
|
156
|
+
the decoder represents faithfully remain subject to the existing filter and
|
|
157
|
+
blocked-link policy. UNIX creator metadata and permission defaults remain as
|
|
158
|
+
described above.
|
|
143
159
|
|
|
144
160
|
Within one ZIP entry, identical local and central name bytes reuse the same
|
|
145
161
|
decoded validation. Unicode Path admission is shared only when both the raw names
|
|
@@ -232,7 +248,7 @@ omitted and do not consume output payload budgets. The shared core admits these
|
|
|
232
248
|
record and are then cleared; local PAX on unsupported types and GNU sparse
|
|
233
249
|
`S` records retain their existing fail-closed format policy.
|
|
234
250
|
|
|
235
|
-
If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
|
|
251
|
+
If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. On Windows, archive source and destination filesystem paths reject NTFS alternate-stream and directory-index namespace spellings such as `file:stream` and `dir::$INDEX_ALLOCATION`; ordinary colon-bearing POSIX names remain valid. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
|
|
236
252
|
|
|
237
253
|
Extraction captures the destination's lossless filesystem identity before any
|
|
238
254
|
entry filter runs and retains that capability through final publication. If a
|
|
@@ -321,6 +337,11 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
|
|
|
321
337
|
- **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget for non-mutating work. Extraction is aborted on overrun; if a destination mutation is already in flight, that mutation and rollback are joined before rejection so archive-controlled publication cannot continue afterward.
|
|
322
338
|
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before buffering their bodies. It understands octal and base-256 fixed sizes and validates bounded local PAX bodies before using their size overrides for member framing. Original archive bytes remain unchanged.
|
|
323
339
|
|
|
340
|
+
Native gzip, zstd, and bzip2 readers check cancellation before refilling
|
|
341
|
+
compressed input and before each decoded read, including buffered output. These checks
|
|
342
|
+
apply to file extraction and in-memory member reads; they cannot interrupt an
|
|
343
|
+
already-running filesystem read or a decoder step using already-buffered input.
|
|
344
|
+
|
|
324
345
|
### Raw TAR framing
|
|
325
346
|
|
|
326
347
|
Extraction and bounded reads admit the complete decoded TAR stream through the
|
package/docs/atomic.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Atomic writes
|
|
2
2
|
|
|
3
|
-
`@openclaw/fs-safe/atomic` re-exports the lower-level helpers that `root()`'s write methods are built on. Reach for them when you have
|
|
3
|
+
`@openclaw/fs-safe/atomic` re-exports the lower-level helpers that `root()`'s write methods are built on. Reach for them when you have a path you trust and want sibling-temp + rename without setting up a `Root`, or when you need finer control over `fsync`, mode preservation, or pre-rename hooks.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import {
|
|
@@ -20,6 +20,14 @@ On POSIX, the parent is opened with no-follow and directory-only flags, checked
|
|
|
20
20
|
|
|
21
21
|
Async replacements to the same destination are serialized inside the current process, so two overlapping `replaceFileAtomic()` calls do not interleave their temp-write/rename phases. Use a sidecar lock when multiple processes may write the same target.
|
|
22
22
|
|
|
23
|
+
Relative paths retain their literal suffix so `.` and `..` keep Node's existing
|
|
24
|
+
filesystem semantics. On Windows, an ordinary drive-relative destination such
|
|
25
|
+
as `C:.\\state.json` is captured at entry by anchoring the drive's current
|
|
26
|
+
directory without normalizing that suffix. The anchored absolute spelling is
|
|
27
|
+
used for staging, locks, callbacks, publication, and cleanup. Any additional
|
|
28
|
+
colon remains visible and is rejected as a filesystem namespace alias before
|
|
29
|
+
I/O; malformed namespace-drive spellings remain rejected.
|
|
30
|
+
|
|
23
31
|
```ts
|
|
24
32
|
import { replaceFileAtomic } from "@openclaw/fs-safe/atomic";
|
|
25
33
|
|
|
@@ -174,6 +182,8 @@ await replaceDirectoryAtomic({
|
|
|
174
182
|
The helper renames `targetDir` to a generated backup path, renames `stagedDir → targetDir`, then removes the backup. If the second rename fails, it tries to restore the original target before rethrowing.
|
|
175
183
|
Concurrent replacements of the same resolved target are serialized inside the
|
|
176
184
|
current process so their backup, commit, and cleanup phases cannot interleave.
|
|
185
|
+
On Windows, ordinary drive-relative staged and target paths are anchored at
|
|
186
|
+
entry before namespace-alias admission and resolution.
|
|
177
187
|
`backupPrefix`, when supplied, is sanitized as one path prefix and cannot contain
|
|
178
188
|
path separators or NUL bytes; the generated backup tail is randomized.
|
|
179
189
|
|
|
@@ -248,6 +258,8 @@ await movePathWithCopyFallback({
|
|
|
248
258
|
```
|
|
249
259
|
|
|
250
260
|
Use it when source and destination might live on different filesystems (containers, tmpfs, separate volumes).
|
|
261
|
+
On Windows, ordinary drive-relative `from` and `to` paths are anchored at entry;
|
|
262
|
+
publication receipts report the resulting absolute destination.
|
|
251
263
|
The hardlink policy is captured when the move starts. Changing or reusing the
|
|
252
264
|
options object later does not change the policy of an in-flight move.
|
|
253
265
|
`sourceHardlinks: "reject"` performs a recursive preflight capped at 50,000
|
|
@@ -260,7 +272,19 @@ the preflight cap fails with `FsSafeError("too-large")`.
|
|
|
260
272
|
If another writer changes source entries during the fallback, the staged copy
|
|
261
273
|
throws `ESTALE` before commit when possible. If the destination has already
|
|
262
274
|
been committed, cleanup still preserves the changed source entries and throws
|
|
263
|
-
`ESTALE`.
|
|
275
|
+
`ESTALE`. Directory manifests retain an exact bigint device/inode receipt from
|
|
276
|
+
copy admission. Each directory is rechecked after traversal, and the source root
|
|
277
|
+
is checked again before publication. Cleanup checks the same receipt before
|
|
278
|
+
removing children, then invokes mutation authority and rechecks the receipt and
|
|
279
|
+
directory type immediately before removal. Unknown Windows identity
|
|
280
|
+
components get at most one retry that retains known components; persistent
|
|
281
|
+
ambiguity fails closed before further removal. A directory that disappeared or
|
|
282
|
+
was replaced during child cleanup is reported as stale; an observed replacement
|
|
283
|
+
is preserved. Unrelated children
|
|
284
|
+
added to the original directory are preserved while unchanged copied children
|
|
285
|
+
are still removed. These pathname checks remain best-effort: they cannot make
|
|
286
|
+
the final identity check and removal atomic against another process.
|
|
287
|
+
When allowed source names are hardlinks to the same inode, each owned
|
|
264
288
|
unlink is verified through a remaining manifested alias and its exact resulting
|
|
265
289
|
identity becomes the next cleanup receipt. This accounts for the operation's
|
|
266
290
|
own link-count and ctime changes without suppressing unexpected external
|
|
@@ -337,7 +361,7 @@ preserves the existing move and source-identity behavior.
|
|
|
337
361
|
|
|
338
362
|
| `Root` methods | `atomic` helpers |
|
|
339
363
|
|---|---|
|
|
340
|
-
| Take relative paths, bound to a `rootDir`. | Take absolute paths, no boundary. |
|
|
364
|
+
| Take relative paths, bound to a `rootDir`. | Take trusted absolute or relative paths, no boundary. |
|
|
341
365
|
| Throw `FsSafeError` with `code`. | Throw `FsSafeError` *or* the underlying `NodeJS.ErrnoException`, depending on failure point. |
|
|
342
366
|
| Atomicity, mode, hooks, fsync are sane defaults. | Caller controls all of the above. |
|
|
343
367
|
| `mkdir`, identity check, hardlink reject built in. | No root boundary; `movePathWithCopyFallback` has explicit `sourceHardlinks` policy, while other helpers expose their own narrower checks. |
|
package/docs/copy.md
CHANGED
|
@@ -97,6 +97,9 @@ const bytes = await copyFileHandle(sourceHandle, targetHandle, {
|
|
|
97
97
|
|
|
98
98
|
`CopyFileHandleOptions` contains optional `maxBytes`, `signal`, `onChunk`, and
|
|
99
99
|
`assertBeforeMutation`. The result is the actual byte count copied through EOF.
|
|
100
|
+
The four options are selected once before descriptor inspection; later mutation
|
|
101
|
+
of the options object cannot replace the active budget, signal, observer, or
|
|
102
|
+
mutation-authority callback.
|
|
100
103
|
The byte limit is not a prefix length: excess data rejects with `too-large`,
|
|
101
104
|
including data added after admission. Omitted limits are unlimited; Root's
|
|
102
105
|
default read cap does not apply. Zero accepts only an empty source. Invalid
|
package/docs/durability.md
CHANGED
|
@@ -53,6 +53,14 @@ propagate.
|
|
|
53
53
|
discard both unsupported outcomes and failures. Use them only when the primary
|
|
54
54
|
write remains useful without a crash-durability promise.
|
|
55
55
|
|
|
56
|
+
On Windows, pathname inputs and supplied directory receipts reject NTFS
|
|
57
|
+
alternate-stream and directory-index namespace spellings before opening,
|
|
58
|
+
creating, hashing, or publishing anything. This applies to directory
|
|
59
|
+
durability, `publishFileExclusive()`, and the pathname overloads of
|
|
60
|
+
`sha256File()` and `sha256FileSync()`; the already-open `FileHandle` and
|
|
61
|
+
borrowed numeric file-descriptor overloads are unchanged. Ordinary colon-bearing
|
|
62
|
+
POSIX paths remain valid.
|
|
63
|
+
|
|
56
64
|
## Pinned directories
|
|
57
65
|
|
|
58
66
|
`pinDirectory()` rejects final symlinks and non-directories. On POSIX it opens
|
|
@@ -94,6 +102,12 @@ target. It pins the source with nonblocking `O_NOFOLLOW`, optionally verifies
|
|
|
94
102
|
`expectedSourceIdentity`, tries a hardlink first, then synchronizes the target
|
|
95
103
|
parent directory.
|
|
96
104
|
|
|
105
|
+
Trusted relative source and target paths are resolved to absolute paths before
|
|
106
|
+
authority checks. On Windows, ordinary drive-relative operands are anchored at
|
|
107
|
+
entry before namespace-alias admission. A caller-supplied `parentReceipt` must
|
|
108
|
+
still name the resolved target parent and is not relaxed by this compatibility
|
|
109
|
+
rule.
|
|
110
|
+
|
|
97
111
|
For example, a backup archive is complete before publication. If directory
|
|
98
112
|
sync fails, keeping that complete file is more useful than conditionally
|
|
99
113
|
deleting it by pathname:
|
package/docs/errors.md
CHANGED
|
@@ -38,6 +38,14 @@ class FsSafeError extends Error {
|
|
|
38
38
|
|
|
39
39
|
`cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
|
|
40
40
|
|
|
41
|
+
Guarded write preparation describes permission, read-only filesystem, and disk-space
|
|
42
|
+
failures with messages such as `permission denied (EACCES)` or
|
|
43
|
+
`no space left on device (ENOSPC)`. Other errno failures include their code in
|
|
44
|
+
`filesystem write failed (EIO)`. These wrappers retain the existing `invalid-path`
|
|
45
|
+
code and `policy` category for compatibility, along with the original `cause`;
|
|
46
|
+
they do not expose native message text or paths. Already-classified `FsSafeError`
|
|
47
|
+
instances and missing-path errors keep their existing classification.
|
|
48
|
+
|
|
41
49
|
`details` is an operation-specific receipt, not an alternate error code. For
|
|
42
50
|
example, `publishFileExclusive()` uses it to report the failing phase, created
|
|
43
51
|
target identity, cleanup decision, and failed directory-sync outcome. Narrow
|
|
@@ -114,7 +122,7 @@ type FsSafeErrorCode =
|
|
|
114
122
|
| `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
|
|
115
123
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
116
124
|
| `helper-failed` | A native mechanism or multi-step operational helper failed. | Inspect `cause` and any operation-specific `details`; retrying may be unsafe if the operation partially completed. |
|
|
117
|
-
| `helper-unavailable` |
|
|
125
|
+
| `helper-unavailable` | A required native binding or bounded primitive could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, `FS_SAFE_NATIVE_MODE=off`, or a no-clobber `Root.move()` without safe native parent admission. `auto` falls back only where a safe fallback exists. |
|
|
118
126
|
| `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
|
|
119
127
|
| `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable; a FileStore key used a noncanonical spelling. | Noncanonical FileStore aliases, backslashes, or complete parent segments; a network path on Windows; a drive-relative segment in a portable relative path or store key; or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain broader confined path compatibility, including legal POSIX drive-like names. |
|
|
120
128
|
| `not-empty` | Nonrecursive `remove()` on a non-empty directory, or new children appeared during recursive removal. | Use bounded `recursive: true` removal or coordinate concurrent writers. |
|
|
@@ -136,11 +144,11 @@ type FsSafeErrorCode =
|
|
|
136
144
|
|
|
137
145
|
Secret writes reject invalid `mode` / `dirMode` values with `invalid-path` before directory creation. Existing secret directories with a mode different from the requested `dirMode` report `insecure-permissions` without chmod; a created directory whose descriptor ownership no longer matches its initializing effective user reports `not-owned`.
|
|
138
146
|
|
|
139
|
-
Pathname `sha256File()` also
|
|
140
|
-
or current-path identity remains unknown after one bounded
|
|
141
|
-
if the file is benign.
|
|
142
|
-
report `symlink`, preview or descriptor non-files report
|
|
143
|
-
current-path symlink or non-file reports `path-mismatch`.
|
|
147
|
+
Pathname `sha256File()` and `sha256FileSync()` also report `path-mismatch` when
|
|
148
|
+
pre-open, descriptor, or current-path identity remains unknown after one bounded
|
|
149
|
+
Windows retry, even if the file is benign. Neither reopens to recover identity.
|
|
150
|
+
Preview symlinks report `symlink`, preview or descriptor non-files report
|
|
151
|
+
`not-file`, and a current-path symlink or non-file reports `path-mismatch`.
|
|
144
152
|
|
|
145
153
|
## Branching
|
|
146
154
|
|
package/docs/file-store.md
CHANGED
|
@@ -90,13 +90,16 @@ or converts one caller-supplied key onto another:
|
|
|
90
90
|
are rejected.
|
|
91
91
|
- Windows drive-relative segments such as `C:name` or `C:` are rejected
|
|
92
92
|
anywhere in a key, including `a/C:name`.
|
|
93
|
+
- On Windows, every other colon is rejected too, preventing a key from naming
|
|
94
|
+
an NTFS alternate stream or directory-index alias. POSIX keeps accepting
|
|
95
|
+
ordinary colon-bearing segments that are not drive-relative spellings.
|
|
93
96
|
- No segment may end in an ASCII dot or space.
|
|
94
97
|
|
|
95
98
|
Violations report `invalid-path` when key validation is reached. Ordinary nested
|
|
96
99
|
keys, NFC Unicode such as `café/日本語.txt`, `.hidden`, `a..b`, and internal spaces
|
|
97
|
-
such as `internal space/a b.txt` are accepted.
|
|
98
|
-
timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
|
|
99
|
-
|
|
100
|
+
such as `internal space/a b.txt` are accepted. On POSIX, colons elsewhere, such
|
|
101
|
+
as the timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
|
|
102
|
+
Windows rejects that spelling as stream syntax.
|
|
100
103
|
|
|
101
104
|
Validation retains each method's operation order. Async reads, `exists`, and
|
|
102
105
|
`remove` open the root first: if the root is missing, strict methods report
|
|
@@ -136,6 +139,24 @@ its exact publication identity cannot be verified. There is no equal-content
|
|
|
136
139
|
fallback. The published entry remains present, so callers must inspect or
|
|
137
140
|
recover that outcome instead of assuming the write did not occur.
|
|
138
141
|
|
|
142
|
+
Synchronous directory creation retains exact bigint receipts for the store root
|
|
143
|
+
and every parent component. On POSIX, an existing or newly created directory
|
|
144
|
+
whose complete requested mode differs is reopened without following the final
|
|
145
|
+
name, checked against its receipt and parent chain, and finalized through that
|
|
146
|
+
descriptor. A concurrent root or parent replacement is rejected without
|
|
147
|
+
applying the mode to the replacement. Directories already at the requested mode
|
|
148
|
+
skip the descriptor and mode operation. Windows retains its bounded `mkdir`
|
|
149
|
+
mode request and identity checks without relying on directory descriptors or a
|
|
150
|
+
pathname `chmod`, because Node does not enforce POSIX directory modes there.
|
|
151
|
+
|
|
152
|
+
Node does not expose a portable, `fchmod`-capable search-only directory
|
|
153
|
+
descriptor on Linux. If a mismatched existing directory, or one created under
|
|
154
|
+
an owner-read-removing umask, cannot be opened for reading, the synchronous
|
|
155
|
+
store therefore fails closed with `permission-unverified`; it never falls back
|
|
156
|
+
to pathname `chmod`. On supported macOS x64/arm64 hosts it also tries an
|
|
157
|
+
`O_SEARCH` descriptor, so owner-searchable directories can still be repaired.
|
|
158
|
+
Directories with neither usable read nor search access remain fail-closed.
|
|
159
|
+
|
|
139
160
|
| Method | Durability support |
|
|
140
161
|
|---|---|
|
|
141
162
|
| `write`, `writeText`, `writeJson` (async and sync) | Per-call option overrides store default. |
|
package/docs/filename.md
CHANGED
|
@@ -17,22 +17,27 @@ function sanitizeUntrustedFileName(fileName: string, fallbackName: string): stri
|
|
|
17
17
|
|
|
18
18
|
## What it does
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The primary name goes through this pipeline first:
|
|
21
21
|
|
|
22
|
-
1. **Trim** whitespace.
|
|
22
|
+
1. **Trim** whitespace. An empty result is unusable.
|
|
23
23
|
2. **Strip path components.** Apply `path.posix.basename` then `path.win32.basename` so neither `foo/bar.txt` nor `foo\bar.txt` survives — only the final segment remains.
|
|
24
24
|
3. **Strip non-portable characters.** C0/C1 controls (`0x00`–`0x1f`, `0x7f`–`0x9f`) and the Windows-invalid set `< > : " / \\ | ? *` are removed on every platform.
|
|
25
25
|
4. **Trim again.**
|
|
26
|
-
5.
|
|
27
|
-
6. **
|
|
28
|
-
7. **
|
|
26
|
+
5. An empty result, `"."`, or `".."` is unusable.
|
|
27
|
+
6. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
|
|
28
|
+
7. **Make the final name device-safe.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension on every platform. If the suffix would exceed 200 code units, the unsuffixed tail is shortened first, so truncation cannot recreate a device name.
|
|
29
|
+
|
|
30
|
+
Only when the primary name is unusable does `fallbackName` go through the same
|
|
31
|
+
nonrecursive pipeline. A safe fallback is preserved exactly; path components,
|
|
32
|
+
controls, reserved device names, and overlong fallback names receive the same
|
|
33
|
+
treatment as the primary name. If both candidates are unusable, the function
|
|
34
|
+
returns the fixed safe literal `"file"`.
|
|
29
35
|
|
|
30
36
|
If truncation itself exposes a reserved-device basename after Windows ignores
|
|
31
37
|
trailing spaces or dots, the result is shortened once more and receives the
|
|
32
38
|
same underscore suffix. A name that reaches the sanitization branch therefore
|
|
33
39
|
remains at most 200 UTF-16 code units and is never a Windows reserved-device
|
|
34
|
-
alias.
|
|
35
|
-
callers must supply a fallback that already satisfies their filename policy.
|
|
40
|
+
alias. Fallback names pass through the same checks before they can be returned.
|
|
36
41
|
|
|
37
42
|
That's it. The function stays intentionally small: it removes traversal and
|
|
38
43
|
the most obvious cross-platform device and character hazards, but it is not a
|
|
@@ -48,6 +53,8 @@ sanitizeUntrustedFileName("a\u0000b\tc", "upload"); // "abc"
|
|
|
48
53
|
sanitizeUntrustedFileName(" ", "fallback"); // "fallback"
|
|
49
54
|
sanitizeUntrustedFileName(".", "fallback"); // "fallback"
|
|
50
55
|
sanitizeUntrustedFileName("..", "fallback"); // "fallback"
|
|
56
|
+
sanitizeUntrustedFileName("<>", "../../etc/passwd"); // "passwd"
|
|
57
|
+
sanitizeUntrustedFileName("<>", "../.."); // "file"
|
|
51
58
|
sanitizeUntrustedFileName("CON", "fallback"); // "CON_"
|
|
52
59
|
sanitizeUntrustedFileName("nul.txt", "fallback"); // "nul_.txt"
|
|
53
60
|
sanitizeUntrustedFileName("aux.c", "fallback"); // "aux_.c"
|