@openclaw/fs-safe 0.5.6 → 0.7.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 +89 -0
- package/README.md +23 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-kind.js +2 -2
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +31 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +88 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +135 -19
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +4 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +63 -42
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +26 -9
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +15 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +22 -6
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +26 -37
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +118 -98
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +49 -120
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +51 -72
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +42 -29
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +19 -2
- package/docs/archive.md +250 -35
- package/docs/atomic.md +11 -2
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +18 -5
- package/docs/errors.md +16 -1
- package/docs/file-store.md +2 -0
- package/docs/index.md +3 -1
- package/docs/install.md +20 -8
- package/docs/json.md +8 -4
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +30 -8
- package/docs/native.md +73 -17
- package/docs/output.md +10 -0
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +3 -1
- package/docs/secret-file.md +12 -0
- package/docs/secure-file.md +21 -3
- package/docs/security-model.md +33 -1
- package/docs/sidecar-lock.md +15 -1
- package/docs/staged-file.md +178 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/root-impl.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
6
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
8
|
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
@@ -9,7 +10,7 @@ import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.j
|
|
|
9
10
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
10
11
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
11
12
|
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
12
|
-
import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
13
|
+
import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
13
14
|
import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
|
|
14
15
|
import { getNativeBinding } from "./native.js";
|
|
15
16
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
@@ -25,6 +26,8 @@ import { stringifyJsonDocument } from "./json-stringify.js";
|
|
|
25
26
|
import { walkRoot } from "./root-walk.js";
|
|
26
27
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
27
28
|
import { serializePathWrite } from "./write-queue.js";
|
|
29
|
+
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
30
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
28
31
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
29
32
|
function logWarn(message) {
|
|
30
33
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
@@ -74,22 +77,27 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
74
77
|
assertNoUnsafeDeviceReadPath(filePath);
|
|
75
78
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
76
79
|
let preOpenStat;
|
|
80
|
+
let observedBeforeOpen = false;
|
|
77
81
|
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
78
82
|
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
79
83
|
try {
|
|
80
|
-
preOpenStat = await
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
preOpenStat = await inspectFileIdentity(async () => {
|
|
85
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
86
|
+
observedBeforeOpen = true;
|
|
87
|
+
if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
88
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
89
|
+
}
|
|
90
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) {
|
|
91
|
+
throw new FsSafeError("not-file", "not a file");
|
|
92
|
+
}
|
|
93
|
+
return stat;
|
|
94
|
+
});
|
|
87
95
|
}
|
|
88
96
|
catch (err) {
|
|
89
|
-
if (err instanceof FsSafeError) {
|
|
97
|
+
if (err instanceof FsSafeError || observedBeforeOpen) {
|
|
90
98
|
throw err;
|
|
91
99
|
}
|
|
92
|
-
//
|
|
100
|
+
// Only an initial lookup failure falls through; a failed re-inspection aborts.
|
|
93
101
|
}
|
|
94
102
|
if (preOpenStat) {
|
|
95
103
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
@@ -127,38 +135,30 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
127
135
|
if (!stat.isFile()) {
|
|
128
136
|
throw new FsSafeError("not-file", "not a file");
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
!sameFileIdentity(stat, preOpenStat)) {
|
|
133
|
-
throw new FsSafeError("path-mismatch", "path changed before open");
|
|
134
|
-
}
|
|
138
|
+
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
139
|
+
const identity = await inspectFileIdentity(() => handle.stat({ bigint: true }), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined);
|
|
135
140
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
136
141
|
throw hardlinkedPathNotAllowedError();
|
|
137
142
|
}
|
|
138
|
-
|
|
139
|
-
const pathStat =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
const pathStat = await fs.lstat(filePath);
|
|
146
|
-
if (pathStat.isSymbolicLink()) {
|
|
143
|
+
await inspectFileIdentity(async () => {
|
|
144
|
+
const pathStat = options?.symlinks === "follow-within-root"
|
|
145
|
+
? await fs.stat(filePath, { bigint: true })
|
|
146
|
+
: await fs.lstat(filePath, { bigint: true });
|
|
147
|
+
if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
147
148
|
throw new FsSafeError("symlink", "symlink not allowed");
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
return pathStat;
|
|
151
|
+
}, identity);
|
|
152
|
+
await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
|
|
153
|
+
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath);
|
|
154
|
+
await inspectFileIdentity(async () => {
|
|
155
|
+
const realStat = await fs.stat(realPath, { bigint: true });
|
|
156
|
+
if (options?.hardlinks === "reject" && realStat.nlink > 1n) {
|
|
157
|
+
throw hardlinkedPathNotAllowedError();
|
|
151
158
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (options?.hardlinks === "reject" && realStat.nlink > 1) {
|
|
156
|
-
throw hardlinkedPathNotAllowedError();
|
|
157
|
-
}
|
|
158
|
-
if (!sameFileIdentity(stat, realStat)) {
|
|
159
|
-
throw new FsSafeError("path-mismatch", "path mismatch");
|
|
160
|
-
}
|
|
161
|
-
return openResult({ handle, realPath, stat });
|
|
159
|
+
return realStat;
|
|
160
|
+
}, identity);
|
|
161
|
+
return { opened: openResult({ handle, realPath, stat }), identity };
|
|
162
162
|
}
|
|
163
163
|
catch (err) {
|
|
164
164
|
await handle.close().catch(() => { });
|
|
@@ -212,8 +212,7 @@ class RootHandle {
|
|
|
212
212
|
async read(relativePath, options = {}) {
|
|
213
213
|
return await readFileInRoot(this.context, {
|
|
214
214
|
relativePath,
|
|
215
|
-
...
|
|
216
|
-
...options,
|
|
215
|
+
...mergeReadOptions(this.defaults, options),
|
|
217
216
|
});
|
|
218
217
|
}
|
|
219
218
|
async readBytes(relativePath, options = {}) {
|
|
@@ -229,8 +228,7 @@ class RootHandle {
|
|
|
229
228
|
async readAbsolute(filePath, options = {}) {
|
|
230
229
|
return await readPathInRoot(this.context, {
|
|
231
230
|
filePath,
|
|
232
|
-
...
|
|
233
|
-
...options,
|
|
231
|
+
...mergeReadOptions(this.defaults, options),
|
|
234
232
|
});
|
|
235
233
|
}
|
|
236
234
|
reader(options = {}) {
|
|
@@ -315,13 +313,14 @@ class RootHandle {
|
|
|
315
313
|
}
|
|
316
314
|
async copyIn(relativePath, sourcePath, options = {}) {
|
|
317
315
|
assertValidRootDestinationPath(relativePath);
|
|
316
|
+
const { maxBytes, ...copyOptions } = this.mutationOptions(options);
|
|
318
317
|
await copyFileInRoot(this.context, {
|
|
319
318
|
sourcePath,
|
|
320
319
|
relativePath,
|
|
321
|
-
maxBytes: this.defaults.maxBytes,
|
|
320
|
+
maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
|
|
322
321
|
mkdir: this.defaults.mkdir,
|
|
323
322
|
mode: this.defaults.mode,
|
|
324
|
-
...
|
|
323
|
+
...copyOptions,
|
|
325
324
|
});
|
|
326
325
|
}
|
|
327
326
|
async exists(relativePath) {
|
|
@@ -371,12 +370,24 @@ class RootHandle {
|
|
|
371
370
|
function readDefaults(defaults) {
|
|
372
371
|
return {
|
|
373
372
|
hardlinks: defaults.hardlinks,
|
|
374
|
-
maxBytes: defaults.maxBytes
|
|
373
|
+
maxBytes: normalizeMaxBytes(defaults.maxBytes, { defaultValue: DEFAULT_ROOT_MAX_BYTES }),
|
|
375
374
|
nonBlockingRead: defaults.nonBlockingRead,
|
|
376
375
|
symlinks: defaults.symlinks,
|
|
377
376
|
};
|
|
378
377
|
}
|
|
378
|
+
function mergeReadOptions(defaults, options) {
|
|
379
|
+
const merged = readDefaults(defaults);
|
|
380
|
+
if (options.hardlinks !== undefined)
|
|
381
|
+
merged.hardlinks = options.hardlinks;
|
|
382
|
+
merged.maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: merged.maxBytes });
|
|
383
|
+
if (options.nonBlockingRead !== undefined)
|
|
384
|
+
merged.nonBlockingRead = options.nonBlockingRead;
|
|
385
|
+
if (options.symlinks !== undefined)
|
|
386
|
+
merged.symlinks = options.symlinks;
|
|
387
|
+
return merged;
|
|
388
|
+
}
|
|
379
389
|
export async function root(rootDir, defaults = {}) {
|
|
390
|
+
normalizeMaxBytes(defaults.maxBytes);
|
|
380
391
|
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
381
392
|
}
|
|
382
393
|
async function openFileInRoot(root, params) {
|
|
@@ -387,10 +398,10 @@ async function openFileInRoot(root, params) {
|
|
|
387
398
|
});
|
|
388
399
|
let opened;
|
|
389
400
|
try {
|
|
390
|
-
opened = await openVerifiedLocalFile(resolved, {
|
|
401
|
+
({ opened } = await openVerifiedLocalFile(resolved, {
|
|
391
402
|
nonBlockingRead: params.nonBlockingRead,
|
|
392
403
|
symlinks: params.symlinks,
|
|
393
|
-
});
|
|
404
|
+
}));
|
|
394
405
|
}
|
|
395
406
|
catch (err) {
|
|
396
407
|
if (err instanceof FsSafeError) {
|
|
@@ -432,9 +443,10 @@ async function readPathInRoot(root, params) {
|
|
|
432
443
|
});
|
|
433
444
|
}
|
|
434
445
|
export async function readLocalFileSafely(params) {
|
|
446
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
435
447
|
const opened = await openLocalFileSafely({ filePath: params.filePath });
|
|
436
448
|
try {
|
|
437
|
-
return await readOpenedFileSafely({ opened, maxBytes
|
|
449
|
+
return await readOpenedFileSafely({ opened, maxBytes });
|
|
438
450
|
}
|
|
439
451
|
finally {
|
|
440
452
|
await opened.handle.close().catch(() => { });
|
|
@@ -442,7 +454,7 @@ export async function readLocalFileSafely(params) {
|
|
|
442
454
|
}
|
|
443
455
|
export async function openLocalFileSafely(params) {
|
|
444
456
|
assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
|
|
445
|
-
return await openVerifiedLocalFile(params.filePath);
|
|
457
|
+
return (await openVerifiedLocalFile(params.filePath)).opened;
|
|
446
458
|
}
|
|
447
459
|
function emitWriteBoundaryWarning(reason) {
|
|
448
460
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
@@ -474,26 +486,13 @@ async function writeTempFileForAtomicReplace(params) {
|
|
|
474
486
|
await tempHandle.writeFile(params.data);
|
|
475
487
|
}
|
|
476
488
|
return {
|
|
477
|
-
|
|
478
|
-
|
|
489
|
+
handle: tempHandle,
|
|
490
|
+
identity: await tempHandle.stat({ bigint: true }),
|
|
479
491
|
};
|
|
480
492
|
}
|
|
481
|
-
|
|
493
|
+
catch (error) {
|
|
482
494
|
await tempHandle.close().catch(() => { });
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
async function verifyAtomicWriteResult(params) {
|
|
486
|
-
const opened = await openVerifiedLocalFile(params.targetPath, { hardlinks: "reject" });
|
|
487
|
-
try {
|
|
488
|
-
if (!sameFileIdentity(opened.stat, params.expectedIdentity)) {
|
|
489
|
-
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
490
|
-
}
|
|
491
|
-
if (!isPathInside(params.root.rootWithSep, opened.realPath)) {
|
|
492
|
-
throw outsideWorkspaceError();
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
finally {
|
|
496
|
-
await opened.handle.close().catch(() => { });
|
|
495
|
+
throw error;
|
|
497
496
|
}
|
|
498
497
|
}
|
|
499
498
|
async function resolveGuardedWritePathInRoot(root, params) {
|
|
@@ -709,9 +708,9 @@ async function writeFileInRoot(root, params) {
|
|
|
709
708
|
});
|
|
710
709
|
}
|
|
711
710
|
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
712
|
-
let
|
|
711
|
+
let verifyingPublication = false;
|
|
713
712
|
try {
|
|
714
|
-
|
|
713
|
+
await runPinnedWriteWithRenamePolicy({
|
|
715
714
|
rootPath: pinned.rootReal,
|
|
716
715
|
relativeParentPath: pinned.relativeParentPath,
|
|
717
716
|
basename: pinned.basename,
|
|
@@ -722,9 +721,27 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
722
721
|
overwrite: params.overwrite,
|
|
723
722
|
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
724
723
|
rootIdentity: root.rootIdentity,
|
|
724
|
+
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
725
|
+
verifyingPublication = true;
|
|
726
|
+
try {
|
|
727
|
+
await verifyAtomicWriteResult({
|
|
728
|
+
root,
|
|
729
|
+
targetPath: pinned.targetPath,
|
|
730
|
+
fd,
|
|
731
|
+
expectedIdentity,
|
|
732
|
+
parentGuard,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(error)}`);
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
},
|
|
725
740
|
});
|
|
726
741
|
}
|
|
727
742
|
catch (error) {
|
|
743
|
+
if (verifyingPublication)
|
|
744
|
+
throw error;
|
|
728
745
|
const errorCode = error?.code;
|
|
729
746
|
if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
|
|
730
747
|
throw error;
|
|
@@ -736,22 +753,11 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
736
753
|
}
|
|
737
754
|
throw normalizePinnedWriteError(error);
|
|
738
755
|
}
|
|
739
|
-
try {
|
|
740
|
-
await verifyAtomicWriteResult({
|
|
741
|
-
root,
|
|
742
|
-
targetPath: pinned.targetPath,
|
|
743
|
-
expectedIdentity: identity,
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
catch (err) {
|
|
747
|
-
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
748
|
-
throw err;
|
|
749
|
-
}
|
|
750
756
|
}
|
|
751
757
|
async function copyFileInRoot(root, params) {
|
|
752
758
|
assertValidRootRelativePath(params.relativePath);
|
|
753
759
|
assertNoNulPathInput(params.sourcePath, "source path contains a NUL byte");
|
|
754
|
-
const source = await openVerifiedLocalFile(params.sourcePath, {
|
|
760
|
+
const { opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.sourcePath, {
|
|
755
761
|
hardlinks: params.sourceHardlinks,
|
|
756
762
|
});
|
|
757
763
|
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
@@ -762,7 +768,7 @@ async function copyFileInRoot(root, params) {
|
|
|
762
768
|
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
763
769
|
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
764
770
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
765
|
-
await assertCopySourceCurrent(source);
|
|
771
|
+
await assertCopySourceCurrent(source, sourceIdentity);
|
|
766
772
|
let identity;
|
|
767
773
|
try {
|
|
768
774
|
identity = await runPinnedWriteHelper({
|
|
@@ -781,7 +787,7 @@ async function copyFileInRoot(root, params) {
|
|
|
781
787
|
throw normalizePinnedWriteError(error);
|
|
782
788
|
}
|
|
783
789
|
try {
|
|
784
|
-
await assertCopySourcePathCurrent(source);
|
|
790
|
+
await assertCopySourcePathCurrent(source, sourceIdentity);
|
|
785
791
|
}
|
|
786
792
|
catch (error) {
|
|
787
793
|
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
@@ -794,18 +800,18 @@ async function copyFileInRoot(root, params) {
|
|
|
794
800
|
await source.handle.close().catch(() => { });
|
|
795
801
|
}
|
|
796
802
|
}
|
|
797
|
-
async function assertCopySourceCurrent(source) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
throw new FsSafeError("path-mismatch", "copy source descriptor changed");
|
|
801
|
-
}
|
|
802
|
-
await assertCopySourcePathCurrent(source);
|
|
803
|
+
async function assertCopySourceCurrent(source, identity) {
|
|
804
|
+
await inspectFileIdentity(() => source.handle.stat({ bigint: true }), identity);
|
|
805
|
+
await assertCopySourcePathCurrent(source, identity);
|
|
803
806
|
}
|
|
804
|
-
async function assertCopySourcePathCurrent(source) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
807
|
+
async function assertCopySourcePathCurrent(source, identity) {
|
|
808
|
+
await inspectFileIdentity(async () => {
|
|
809
|
+
const current = await fs.lstat(source.realPath, { bigint: true });
|
|
810
|
+
if (current.isSymbolicLink() || !current.isFile()) {
|
|
811
|
+
throw new FsSafeError("path-mismatch", "copy source path changed");
|
|
812
|
+
}
|
|
813
|
+
return current;
|
|
814
|
+
}, identity);
|
|
809
815
|
}
|
|
810
816
|
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
811
817
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
@@ -863,7 +869,7 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
863
869
|
targetPath: resolved,
|
|
864
870
|
relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
|
|
865
871
|
basename,
|
|
866
|
-
mode
|
|
872
|
+
mode,
|
|
867
873
|
};
|
|
868
874
|
}
|
|
869
875
|
async function resolvePinnedPathInRoot(root, params) {
|
|
@@ -1108,6 +1114,7 @@ async function writeFileFallback(root, params) {
|
|
|
1108
1114
|
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
|
|
1109
1115
|
let tempPath = null;
|
|
1110
1116
|
let unregisterTempPath = null;
|
|
1117
|
+
let writtenHandle;
|
|
1111
1118
|
try {
|
|
1112
1119
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1113
1120
|
unregisterTempPath = registerTempPathForExit(tempPath);
|
|
@@ -1115,14 +1122,15 @@ async function writeFileFallback(root, params) {
|
|
|
1115
1122
|
tempPath,
|
|
1116
1123
|
data: params.data,
|
|
1117
1124
|
encoding: params.encoding,
|
|
1118
|
-
mode
|
|
1125
|
+
mode,
|
|
1119
1126
|
});
|
|
1120
|
-
|
|
1127
|
+
writtenHandle = written.handle;
|
|
1128
|
+
unregisterTempPath.setIdentity(written.identity);
|
|
1121
1129
|
const commitTempPath = tempPath;
|
|
1122
1130
|
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
1123
1131
|
await fs.rename(commitTempPath, destinationPath);
|
|
1132
|
+
tempPath = null;
|
|
1124
1133
|
});
|
|
1125
|
-
tempPath = null;
|
|
1126
1134
|
unregisterTempPath();
|
|
1127
1135
|
unregisterTempPath = null;
|
|
1128
1136
|
try {
|
|
@@ -1130,6 +1138,8 @@ async function writeFileFallback(root, params) {
|
|
|
1130
1138
|
root,
|
|
1131
1139
|
targetPath: destinationPath,
|
|
1132
1140
|
expectedIdentity: written.identity,
|
|
1141
|
+
fd: written.handle.fd,
|
|
1142
|
+
parentGuard: destinationGuard,
|
|
1133
1143
|
});
|
|
1134
1144
|
}
|
|
1135
1145
|
catch (err) {
|
|
@@ -1138,6 +1148,7 @@ async function writeFileFallback(root, params) {
|
|
|
1138
1148
|
}
|
|
1139
1149
|
}
|
|
1140
1150
|
finally {
|
|
1151
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1141
1152
|
if (tempPath) {
|
|
1142
1153
|
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
1143
1154
|
}
|
|
@@ -1155,19 +1166,22 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1155
1166
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
1156
1167
|
let created = false;
|
|
1157
1168
|
let createdIdentity;
|
|
1169
|
+
let writtenHandle;
|
|
1170
|
+
let verifyingPublication = false;
|
|
1158
1171
|
try {
|
|
1159
1172
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1160
1173
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
|
|
1161
1174
|
created = true;
|
|
1162
1175
|
try {
|
|
1163
1176
|
createdIdentity = await handle.stat();
|
|
1177
|
+
const writtenStat = await handle.stat({ bigint: true });
|
|
1164
1178
|
if (typeof params.data === "string") {
|
|
1165
1179
|
await handle.writeFile(params.data, params.encoding ?? "utf8");
|
|
1166
1180
|
}
|
|
1167
1181
|
else {
|
|
1168
1182
|
await handle.writeFile(params.data);
|
|
1169
1183
|
}
|
|
1170
|
-
return { handle, writtenStat
|
|
1184
|
+
return { handle, writtenStat };
|
|
1171
1185
|
}
|
|
1172
1186
|
catch (error) {
|
|
1173
1187
|
await handle.close().catch(() => undefined);
|
|
@@ -1179,15 +1193,20 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1179
1193
|
await handle.close().catch(() => undefined);
|
|
1180
1194
|
},
|
|
1181
1195
|
});
|
|
1182
|
-
|
|
1196
|
+
writtenHandle = handle;
|
|
1197
|
+
created = false;
|
|
1198
|
+
verifyingPublication = true;
|
|
1183
1199
|
await verifyAtomicWriteResult({
|
|
1184
1200
|
root,
|
|
1185
1201
|
targetPath,
|
|
1186
1202
|
expectedIdentity: writtenStat,
|
|
1203
|
+
fd: handle.fd,
|
|
1204
|
+
parentGuard,
|
|
1187
1205
|
});
|
|
1188
|
-
created = false;
|
|
1189
1206
|
}
|
|
1190
1207
|
catch (err) {
|
|
1208
|
+
if (verifyingPublication)
|
|
1209
|
+
throw err;
|
|
1191
1210
|
if (hasNodeErrorCode(err, "EEXIST")) {
|
|
1192
1211
|
throw new FsSafeError("already-exists", "file already exists", {
|
|
1193
1212
|
cause: err instanceof Error ? err : undefined,
|
|
@@ -1196,6 +1215,7 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1196
1215
|
throw err;
|
|
1197
1216
|
}
|
|
1198
1217
|
finally {
|
|
1218
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1199
1219
|
if (created && createdIdentity) {
|
|
1200
1220
|
await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
|
|
1201
1221
|
}
|
package/dist/root-paths.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
1
2
|
type InvalidPathResult = {
|
|
2
3
|
ok: false;
|
|
3
4
|
error: string;
|
|
4
5
|
};
|
|
6
|
+
type DirectoryResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
path: string;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
diagnostic?: FsSafeError;
|
|
13
|
+
};
|
|
5
14
|
type ResolvePathsWithinRootParams = {
|
|
6
15
|
rootDir: string;
|
|
7
16
|
requestedPaths: string[];
|
|
@@ -39,13 +48,7 @@ export type PathScope = {
|
|
|
39
48
|
}>;
|
|
40
49
|
ensureDir(requestedPath: string, options?: PathScopeResolveOptions & {
|
|
41
50
|
mode?: number;
|
|
42
|
-
}): Promise<
|
|
43
|
-
ok: true;
|
|
44
|
-
path: string;
|
|
45
|
-
} | {
|
|
46
|
-
ok: false;
|
|
47
|
-
error: string;
|
|
48
|
-
}>;
|
|
51
|
+
}): Promise<DirectoryResult>;
|
|
49
52
|
};
|
|
50
53
|
export declare function resolvePathWithinRoot(params: {
|
|
51
54
|
rootDir: string;
|
|
@@ -77,13 +80,7 @@ export declare function ensureDirectoryWithinRoot(params: {
|
|
|
77
80
|
scopeLabel: string;
|
|
78
81
|
defaultDirName?: string;
|
|
79
82
|
mode?: number;
|
|
80
|
-
}): Promise<
|
|
81
|
-
ok: true;
|
|
82
|
-
path: string;
|
|
83
|
-
} | {
|
|
84
|
-
ok: false;
|
|
85
|
-
error: string;
|
|
86
|
-
}>;
|
|
83
|
+
}): Promise<DirectoryResult>;
|
|
87
84
|
export declare function resolvePathsWithinRoot(params: ResolvePathsWithinRootParams): ResolvePathsWithinRootResult;
|
|
88
85
|
export declare function resolveExistingPathsWithinRoot(params: ResolvePathsWithinRootParams): Promise<ResolvePathsWithinRootResult>;
|
|
89
86
|
export declare function resolveStrictExistingPathsWithinRoot(params: ResolvePathsWithinRootParams): Promise<ResolvePathsWithinRootResult>;
|
package/dist/root-paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
|
package/dist/root-paths.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { formatErrorDetail } from "./error-detail.js";
|
|
3
4
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
|
|
5
|
+
import { assertNoNulPathInput, hasNodeErrorCode, isNodeError, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
|
|
5
6
|
import { root as openRoot } from "./root.js";
|
|
6
7
|
function invalidPath(scopeLabel) {
|
|
7
8
|
return {
|
|
@@ -126,7 +127,7 @@ async function resolveNearestExistingPath(targetPath) {
|
|
|
126
127
|
async function assertNoSymlinkSegments(params) {
|
|
127
128
|
const relative = path.relative(params.rootDir, params.targetPath);
|
|
128
129
|
if (isPathRelativeEscape(relative)) {
|
|
129
|
-
throw new
|
|
130
|
+
throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
|
|
130
131
|
}
|
|
131
132
|
let current = params.rootDir;
|
|
132
133
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
@@ -134,10 +135,10 @@ async function assertNoSymlinkSegments(params) {
|
|
|
134
135
|
try {
|
|
135
136
|
const stat = await fs.lstat(current);
|
|
136
137
|
if (stat.isSymbolicLink()) {
|
|
137
|
-
throw new
|
|
138
|
+
throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
|
|
138
139
|
}
|
|
139
140
|
if (!stat.isDirectory()) {
|
|
140
|
-
throw new
|
|
141
|
+
throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
catch (err) {
|
|
@@ -149,28 +150,29 @@ async function assertNoSymlinkSegments(params) {
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
export async function ensureDirectoryWithinRoot(params) {
|
|
152
|
-
const
|
|
153
|
-
rootDir: params.rootDir,
|
|
154
|
-
requestedPath: params.requestedPath,
|
|
155
|
-
scopeLabel: params.scopeLabel,
|
|
156
|
-
defaultFileName: params.defaultDirName,
|
|
157
|
-
});
|
|
158
|
-
if (!lexical.ok) {
|
|
159
|
-
return lexical;
|
|
160
|
-
}
|
|
161
|
-
const rootDir = path.resolve(params.rootDir);
|
|
162
|
-
const targetPath = path.resolve(lexical.path);
|
|
153
|
+
const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
|
|
163
154
|
try {
|
|
155
|
+
assertNoNulPathInput(params.rootDir);
|
|
156
|
+
assertNoNulPathInput(params.requestedPath);
|
|
157
|
+
if (!params.requestedPath.trim())
|
|
158
|
+
assertNoNulPathInput(params.defaultDirName ?? "");
|
|
159
|
+
const lexical = resolvePathWithinRoot({
|
|
160
|
+
...params, scopeLabel, defaultFileName: params.defaultDirName,
|
|
161
|
+
});
|
|
162
|
+
if (!lexical.ok)
|
|
163
|
+
return lexical;
|
|
164
|
+
const rootDir = path.resolve(params.rootDir);
|
|
165
|
+
const targetPath = lexical.path;
|
|
164
166
|
const rootStat = await fs.lstat(rootDir);
|
|
165
167
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
166
|
-
return invalidPath(
|
|
168
|
+
return invalidPath(scopeLabel);
|
|
167
169
|
}
|
|
168
|
-
await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel
|
|
170
|
+
await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
|
|
169
171
|
const rootReal = await fs.realpath(rootDir);
|
|
170
172
|
const nearestExistingPath = await resolveNearestExistingPath(targetPath);
|
|
171
173
|
const nearestExistingReal = await fs.realpath(nearestExistingPath);
|
|
172
174
|
if (!isPathInside(rootReal, nearestExistingReal)) {
|
|
173
|
-
return invalidPath(
|
|
175
|
+
return invalidPath(scopeLabel);
|
|
174
176
|
}
|
|
175
177
|
const relative = path.relative(rootDir, targetPath);
|
|
176
178
|
let current = rootDir;
|
|
@@ -180,7 +182,7 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
180
182
|
try {
|
|
181
183
|
const stat = await fs.lstat(current);
|
|
182
184
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
183
|
-
return invalidPath(
|
|
185
|
+
return invalidPath(scopeLabel);
|
|
184
186
|
}
|
|
185
187
|
break;
|
|
186
188
|
}
|
|
@@ -192,10 +194,7 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
192
194
|
await fs.mkdir(current, { mode: params.mode });
|
|
193
195
|
}
|
|
194
196
|
catch (mkdirErr) {
|
|
195
|
-
if (
|
|
196
|
-
throw mkdirErr;
|
|
197
|
-
}
|
|
198
|
-
if (mkdirErr.code === "EEXIST") {
|
|
197
|
+
if (hasNodeErrorCode(mkdirErr, "EEXIST")) {
|
|
199
198
|
continue;
|
|
200
199
|
}
|
|
201
200
|
throw mkdirErr;
|
|
@@ -204,17 +203,27 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
204
203
|
}
|
|
205
204
|
const currentReal = await fs.realpath(current);
|
|
206
205
|
if (!isPathInside(rootReal, currentReal)) {
|
|
207
|
-
return invalidPath(
|
|
206
|
+
return invalidPath(scopeLabel);
|
|
208
207
|
}
|
|
209
208
|
}
|
|
210
209
|
const targetReal = await fs.realpath(targetPath);
|
|
211
210
|
if (!isPathInside(rootReal, targetReal)) {
|
|
212
|
-
return invalidPath(
|
|
211
|
+
return invalidPath(scopeLabel);
|
|
213
212
|
}
|
|
214
213
|
return { ok: true, path: targetPath };
|
|
215
214
|
}
|
|
216
|
-
catch {
|
|
217
|
-
|
|
215
|
+
catch (cause) {
|
|
216
|
+
if ((cause instanceof FsSafeError && cause.category === "policy") ||
|
|
217
|
+
hasNodeErrorCode(cause, "ENOTDIR")) {
|
|
218
|
+
return invalidPath(scopeLabel);
|
|
219
|
+
}
|
|
220
|
+
// Native messages include unbounded paths; display only bounded errno metadata.
|
|
221
|
+
const code = isNodeError(cause) && typeof cause.code === "string"
|
|
222
|
+
? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
|
|
223
|
+
const syscall = isNodeError(cause) && typeof cause.syscall === "string"
|
|
224
|
+
? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
|
|
225
|
+
const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
|
|
226
|
+
return { ok: false, error: diagnostic.message, diagnostic };
|
|
218
227
|
}
|
|
219
228
|
}
|
|
220
229
|
export function resolvePathsWithinRoot(params) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
|
+
import type { PublishedWriteIdentity } from "./pinned-write.js";
|
|
3
|
+
import { type RootContext } from "./root-context.js";
|
|
4
|
+
export declare function verifyAtomicWriteResult(params: {
|
|
5
|
+
root: RootContext;
|
|
6
|
+
targetPath: string;
|
|
7
|
+
fd: number;
|
|
8
|
+
expectedIdentity: PublishedWriteIdentity;
|
|
9
|
+
parentGuard: AsyncDirectoryGuard;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=root-write-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAK3F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,WAAW,EAAE,mBAAmB,CAAC;CAClC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6EhB"}
|