@openclaw/fs-safe 0.5.1 → 0.5.2
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 +66 -0
- package/README.md +18 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +25 -5
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +6 -3
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +30 -12
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +7 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +7 -4
- package/dist/archive-zip-entry.d.ts +1 -0
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-integrity.d.ts +4 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +44 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +6 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +5 -2
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +36 -128
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- 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/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-write.js +2 -1
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +213 -168
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +19 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +31 -7
- package/docs/file-store.md +10 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +17 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +37 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +7 -3
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/dist/root-impl.js
CHANGED
|
@@ -11,14 +11,14 @@ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
|
11
11
|
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
12
12
|
import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
13
13
|
import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
|
|
14
|
+
import { getNativeBinding } from "./native.js";
|
|
14
15
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
15
16
|
import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
16
17
|
import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
17
18
|
import { readOpenedFileSafely } from "./read-opened-file.js";
|
|
18
|
-
import { pathStatFromStats } from "./path-stat.js";
|
|
19
19
|
import { resolveRootPath } from "./root-path.js";
|
|
20
|
-
import { assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, } from "./root-context.js";
|
|
21
|
-
import { isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, } from "./root-errors.js";
|
|
20
|
+
import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, } from "./root-context.js";
|
|
21
|
+
import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, normalizeRemoveGuardError, normalizeRemovePathError, outsideWorkspaceError, } from "./root-errors.js";
|
|
22
22
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
23
23
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
24
24
|
import { walkRoot } from "./root-walk.js";
|
|
@@ -31,11 +31,11 @@ function logWarn(message) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
34
|
-
const NONBLOCK_OPEN_FLAG = "O_NONBLOCK" in fsConstants ? fsConstants.O_NONBLOCK : 0;
|
|
35
|
-
const OPEN_READ_FLAGS = fsConstants.O_RDONLY |
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
34
|
+
const NONBLOCK_OPEN_FLAG = process.platform !== "win32" && "O_NONBLOCK" in fsConstants ? fsConstants.O_NONBLOCK : 0;
|
|
35
|
+
const OPEN_READ_FLAGS = fsConstants.O_RDONLY |
|
|
36
|
+
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0) |
|
|
37
|
+
NONBLOCK_OPEN_FLAG;
|
|
38
|
+
const OPEN_READ_FOLLOW_FLAGS = fsConstants.O_RDONLY | NONBLOCK_OPEN_FLAG;
|
|
39
39
|
const OPEN_WRITE_EXISTING_FLAGS = fsConstants.O_WRONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
40
40
|
const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
|
|
41
41
|
fsConstants.O_CREAT |
|
|
@@ -48,6 +48,21 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
48
48
|
fsConstants.O_EXCL |
|
|
49
49
|
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
50
50
|
export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
51
|
+
function pathStatFromStats(stat) {
|
|
52
|
+
return {
|
|
53
|
+
dev: Number(stat.dev),
|
|
54
|
+
gid: Number(stat.gid),
|
|
55
|
+
ino: Number(stat.ino),
|
|
56
|
+
isDirectory: stat.isDirectory(),
|
|
57
|
+
isFile: stat.isFile(),
|
|
58
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
59
|
+
mode: stat.mode,
|
|
60
|
+
mtimeMs: stat.mtimeMs,
|
|
61
|
+
nlink: stat.nlink,
|
|
62
|
+
size: stat.size,
|
|
63
|
+
uid: stat.uid,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
51
66
|
function openResult(params) {
|
|
52
67
|
return {
|
|
53
68
|
handle: params.handle,
|
|
@@ -60,11 +75,15 @@ function openResult(params) {
|
|
|
60
75
|
async function openVerifiedLocalFile(filePath, options) {
|
|
61
76
|
assertNoUnsafeDeviceReadPath(filePath);
|
|
62
77
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
78
|
+
let preOpenStat;
|
|
63
79
|
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
64
80
|
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
65
81
|
try {
|
|
66
|
-
|
|
67
|
-
if (
|
|
82
|
+
preOpenStat = await fs.lstat(filePath);
|
|
83
|
+
if (preOpenStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
84
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
85
|
+
}
|
|
86
|
+
if (!preOpenStat.isFile() && !preOpenStat.isSymbolicLink()) {
|
|
68
87
|
throw new FsSafeError("not-file", "not a file");
|
|
69
88
|
}
|
|
70
89
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
@@ -78,12 +97,8 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
78
97
|
let handle;
|
|
79
98
|
try {
|
|
80
99
|
const openFlags = options?.symlinks === "follow-within-root"
|
|
81
|
-
?
|
|
82
|
-
|
|
83
|
-
: OPEN_READ_FOLLOW_FLAGS
|
|
84
|
-
: options?.nonBlockingRead
|
|
85
|
-
? OPEN_READ_NONBLOCK_FLAGS
|
|
86
|
-
: OPEN_READ_FLAGS;
|
|
100
|
+
? OPEN_READ_FOLLOW_FLAGS
|
|
101
|
+
: OPEN_READ_FLAGS;
|
|
87
102
|
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
88
103
|
handle = await fs.open(filePath, openFlags);
|
|
89
104
|
try {
|
|
@@ -96,7 +111,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
96
111
|
}
|
|
97
112
|
catch (err) {
|
|
98
113
|
if (isNotFoundPathError(err)) {
|
|
99
|
-
throw
|
|
114
|
+
throw fileNotFoundError();
|
|
100
115
|
}
|
|
101
116
|
if (isSymlinkOpenError(err)) {
|
|
102
117
|
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
@@ -112,8 +127,13 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
112
127
|
if (!stat.isFile()) {
|
|
113
128
|
throw new FsSafeError("not-file", "not a file");
|
|
114
129
|
}
|
|
130
|
+
if (preOpenStat &&
|
|
131
|
+
!preOpenStat.isSymbolicLink() &&
|
|
132
|
+
!sameFileIdentity(stat, preOpenStat)) {
|
|
133
|
+
throw new FsSafeError("path-mismatch", "path changed before open");
|
|
134
|
+
}
|
|
115
135
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
116
|
-
throw
|
|
136
|
+
throw hardlinkedPathNotAllowedError();
|
|
117
137
|
}
|
|
118
138
|
if (options?.symlinks === "follow-within-root") {
|
|
119
139
|
const pathStat = await fs.stat(filePath);
|
|
@@ -133,7 +153,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
133
153
|
const realPath = await resolveOpenedFileRealPathForHandle(handle, filePath);
|
|
134
154
|
const realStat = await fs.stat(realPath);
|
|
135
155
|
if (options?.hardlinks === "reject" && realStat.nlink > 1) {
|
|
136
|
-
throw
|
|
156
|
+
throw hardlinkedPathNotAllowedError();
|
|
137
157
|
}
|
|
138
158
|
if (!sameFileIdentity(stat, realStat)) {
|
|
139
159
|
throw new FsSafeError("path-mismatch", "path mismatch");
|
|
@@ -146,17 +166,19 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
146
166
|
throw err;
|
|
147
167
|
}
|
|
148
168
|
if (isNotFoundPathError(err)) {
|
|
149
|
-
throw
|
|
169
|
+
throw fileNotFoundError();
|
|
150
170
|
}
|
|
151
171
|
throw err;
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
174
|
class RootHandle {
|
|
175
|
+
rootIdentity;
|
|
155
176
|
rootDir;
|
|
156
177
|
rootReal;
|
|
157
178
|
rootWithSep;
|
|
158
179
|
defaults;
|
|
159
180
|
constructor(context, defaults = {}) {
|
|
181
|
+
this.rootIdentity = context.rootIdentity;
|
|
160
182
|
this.rootDir = context.rootDir;
|
|
161
183
|
this.rootReal = context.rootReal;
|
|
162
184
|
this.rootWithSep = context.rootWithSep;
|
|
@@ -165,11 +187,19 @@ class RootHandle {
|
|
|
165
187
|
get context() {
|
|
166
188
|
return {
|
|
167
189
|
rootDir: this.rootDir,
|
|
190
|
+
rootIdentity: this.rootIdentity,
|
|
168
191
|
rootReal: this.rootReal,
|
|
169
192
|
rootWithSep: this.rootWithSep,
|
|
170
193
|
};
|
|
171
194
|
}
|
|
195
|
+
mutationOptions(options) {
|
|
196
|
+
return {
|
|
197
|
+
...options,
|
|
198
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
172
201
|
async resolve(relativePath) {
|
|
202
|
+
assertValidRootDestinationPath(relativePath);
|
|
173
203
|
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
|
|
174
204
|
}
|
|
175
205
|
async open(relativePath, options = {}) {
|
|
@@ -209,67 +239,67 @@ class RootHandle {
|
|
|
209
239
|
};
|
|
210
240
|
}
|
|
211
241
|
async openWritable(relativePath, options = {}) {
|
|
242
|
+
assertValidRootDestinationPath(relativePath);
|
|
212
243
|
const writeMode = options.writeMode ?? "replace";
|
|
213
244
|
return await openWritableFileInRoot(this.context, {
|
|
214
245
|
relativePath,
|
|
215
246
|
mkdir: this.defaults.mkdir,
|
|
216
247
|
mode: this.defaults.mode,
|
|
217
|
-
...options,
|
|
218
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
248
|
+
...this.mutationOptions(options),
|
|
219
249
|
append: writeMode === "append",
|
|
220
250
|
truncateExisting: writeMode === "replace",
|
|
221
251
|
});
|
|
222
252
|
}
|
|
223
253
|
async append(relativePath, data, options = {}) {
|
|
254
|
+
assertValidRootDestinationPath(relativePath);
|
|
224
255
|
await appendFileInRoot(this.context, {
|
|
225
256
|
relativePath,
|
|
226
257
|
data,
|
|
227
258
|
mkdir: this.defaults.mkdir,
|
|
228
259
|
mode: this.defaults.mode,
|
|
229
|
-
...options,
|
|
230
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
260
|
+
...this.mutationOptions(options),
|
|
231
261
|
});
|
|
232
262
|
}
|
|
233
263
|
async remove(relativePath, options = {}) {
|
|
234
264
|
assertValidRootRelativePath(relativePath);
|
|
235
265
|
await removePathInRoot(this.context, {
|
|
236
266
|
relativePath,
|
|
237
|
-
|
|
267
|
+
...this.mutationOptions(options),
|
|
238
268
|
});
|
|
239
269
|
}
|
|
240
270
|
async mkdir(relativePath, options = {}) {
|
|
241
|
-
|
|
271
|
+
assertValidRootDestinationPath(relativePath);
|
|
242
272
|
await mkdirPathInRoot(this.context, {
|
|
243
273
|
relativePath,
|
|
244
|
-
|
|
274
|
+
...this.mutationOptions(options),
|
|
245
275
|
});
|
|
246
276
|
}
|
|
247
277
|
async ensureRoot(options = {}) {
|
|
248
278
|
await mkdirPathInRoot(this.context, {
|
|
249
279
|
relativePath: "",
|
|
250
280
|
allowRoot: true,
|
|
251
|
-
|
|
281
|
+
...this.mutationOptions(options),
|
|
252
282
|
});
|
|
253
283
|
}
|
|
254
284
|
async write(relativePath, data, options = {}) {
|
|
285
|
+
assertValidRootDestinationPath(relativePath);
|
|
255
286
|
await writeFileInRoot(this.context, {
|
|
256
287
|
relativePath,
|
|
257
288
|
data,
|
|
258
289
|
mkdir: this.defaults.mkdir,
|
|
259
290
|
mode: this.defaults.mode,
|
|
260
291
|
renameIdentity: this.defaults.renameIdentity,
|
|
261
|
-
...options,
|
|
262
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
292
|
+
...this.mutationOptions(options),
|
|
263
293
|
});
|
|
264
294
|
}
|
|
265
295
|
async create(relativePath, data, options = {}) {
|
|
296
|
+
assertValidRootDestinationPath(relativePath);
|
|
266
297
|
await writeFileInRoot(this.context, {
|
|
267
298
|
relativePath,
|
|
268
299
|
data,
|
|
269
300
|
mkdir: this.defaults.mkdir,
|
|
270
301
|
mode: this.defaults.mode,
|
|
271
|
-
...options,
|
|
272
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
302
|
+
...this.mutationOptions(options),
|
|
273
303
|
overwrite: false,
|
|
274
304
|
});
|
|
275
305
|
}
|
|
@@ -284,15 +314,14 @@ class RootHandle {
|
|
|
284
314
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
285
315
|
}
|
|
286
316
|
async copyIn(relativePath, sourcePath, options = {}) {
|
|
287
|
-
|
|
317
|
+
assertValidRootDestinationPath(relativePath);
|
|
288
318
|
await copyFileInRoot(this.context, {
|
|
289
319
|
sourcePath,
|
|
290
320
|
relativePath,
|
|
291
321
|
maxBytes: this.defaults.maxBytes,
|
|
292
322
|
mkdir: this.defaults.mkdir,
|
|
293
323
|
mode: this.defaults.mode,
|
|
294
|
-
...options,
|
|
295
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
324
|
+
...this.mutationOptions(options),
|
|
296
325
|
});
|
|
297
326
|
}
|
|
298
327
|
async exists(relativePath) {
|
|
@@ -319,9 +348,9 @@ class RootHandle {
|
|
|
319
348
|
}
|
|
320
349
|
async move(fromRelative, toRelative, options = {}) {
|
|
321
350
|
assertValidRootRelativePath(fromRelative);
|
|
322
|
-
|
|
351
|
+
assertValidRootDestinationPath(toRelative);
|
|
323
352
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
324
|
-
const denyMutations =
|
|
353
|
+
const { denyMutations } = this.mutationOptions(options);
|
|
325
354
|
await assertMoveMutationAllowed(this.context, {
|
|
326
355
|
fromRelative,
|
|
327
356
|
toRelative,
|
|
@@ -353,6 +382,8 @@ export async function root(rootDir, defaults = {}) {
|
|
|
353
382
|
async function openFileInRoot(root, params) {
|
|
354
383
|
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
|
|
355
384
|
allowFinalSymlink: true,
|
|
385
|
+
rejectUnsafeDeviceReads: true,
|
|
386
|
+
rejectSymlinks: params.symlinks !== "follow-within-root",
|
|
356
387
|
});
|
|
357
388
|
let opened;
|
|
358
389
|
try {
|
|
@@ -369,11 +400,11 @@ async function openFileInRoot(root, params) {
|
|
|
369
400
|
}
|
|
370
401
|
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
371
402
|
await opened.handle.close().catch(() => { });
|
|
372
|
-
throw
|
|
403
|
+
throw hardlinkedPathNotAllowedError();
|
|
373
404
|
}
|
|
374
405
|
if (!isPathInside(rootWithSep, opened.realPath)) {
|
|
375
406
|
await opened.handle.close().catch(() => { });
|
|
376
|
-
throw
|
|
407
|
+
throw outsideWorkspaceError();
|
|
377
408
|
}
|
|
378
409
|
return opened;
|
|
379
410
|
}
|
|
@@ -455,33 +486,47 @@ async function verifyAtomicWriteResult(params) {
|
|
|
455
486
|
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
456
487
|
}
|
|
457
488
|
if (!isPathInside(params.root.rootWithSep, opened.realPath)) {
|
|
458
|
-
throw
|
|
489
|
+
throw outsideWorkspaceError();
|
|
459
490
|
}
|
|
460
491
|
}
|
|
461
492
|
finally {
|
|
462
493
|
await opened.handle.close().catch(() => { });
|
|
463
494
|
}
|
|
464
495
|
}
|
|
465
|
-
async function
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
496
|
+
async function resolveGuardedWritePathInRoot(root, params) {
|
|
497
|
+
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
498
|
+
aliasErrorCode: "path-alias",
|
|
499
|
+
allowFinalSymlink: params.allowFinalSymlink,
|
|
500
|
+
});
|
|
501
|
+
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
502
|
+
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
503
|
+
try {
|
|
504
|
+
await assertNoPathAliasEscape({
|
|
505
|
+
absolutePath: resolvedPath.resolved,
|
|
506
|
+
rootPath: resolvedPath.rootReal,
|
|
507
|
+
boundaryLabel: "root",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
throw new FsSafeError("path-alias", "path alias escape blocked", {
|
|
512
|
+
cause: error instanceof Error ? error : undefined,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
477
515
|
}
|
|
516
|
+
return resolvedPath;
|
|
517
|
+
}
|
|
518
|
+
async function openWritableFileInRoot(root, params) {
|
|
519
|
+
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
520
|
+
relativePath: params.relativePath,
|
|
521
|
+
denyMutations: params.denyMutations,
|
|
522
|
+
});
|
|
478
523
|
let ioPath = params.mkdir === false
|
|
479
524
|
? resolved
|
|
480
525
|
: await prepareRootWriteTarget(rootReal, resolved);
|
|
481
526
|
try {
|
|
482
527
|
const resolvedRealPath = await fs.realpath(ioPath);
|
|
483
528
|
if (!isPathInside(rootWithSep, resolvedRealPath)) {
|
|
484
|
-
throw
|
|
529
|
+
throw outsideWorkspaceError();
|
|
485
530
|
}
|
|
486
531
|
ioPath = resolvedRealPath;
|
|
487
532
|
}
|
|
@@ -512,7 +557,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
512
557
|
}
|
|
513
558
|
catch (err) {
|
|
514
559
|
if (isNotFoundPathError(err)) {
|
|
515
|
-
throw
|
|
560
|
+
throw fileNotFoundError();
|
|
516
561
|
}
|
|
517
562
|
if (isSymlinkOpenError(err)) {
|
|
518
563
|
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
@@ -523,13 +568,17 @@ async function openWritableFileInRoot(root, params) {
|
|
|
523
568
|
throw err;
|
|
524
569
|
}
|
|
525
570
|
let realPathForCleanup = null;
|
|
571
|
+
let createdIdentity = null;
|
|
526
572
|
try {
|
|
527
573
|
const stat = await handle.stat();
|
|
574
|
+
if (createdForWrite) {
|
|
575
|
+
createdIdentity = stat;
|
|
576
|
+
}
|
|
528
577
|
if (!stat.isFile()) {
|
|
529
|
-
throw new FsSafeError("
|
|
578
|
+
throw new FsSafeError("not-file", "path is not a regular file under root");
|
|
530
579
|
}
|
|
531
580
|
if (stat.nlink > 1) {
|
|
532
|
-
throw
|
|
581
|
+
throw hardlinkedPathNotAllowedError();
|
|
533
582
|
}
|
|
534
583
|
try {
|
|
535
584
|
const lstat = await fs.lstat(ioPath);
|
|
@@ -552,10 +601,10 @@ async function openWritableFileInRoot(root, params) {
|
|
|
552
601
|
throw new FsSafeError("path-mismatch", "path mismatch");
|
|
553
602
|
}
|
|
554
603
|
if (realStat.nlink > 1) {
|
|
555
|
-
throw
|
|
604
|
+
throw hardlinkedPathNotAllowedError();
|
|
556
605
|
}
|
|
557
606
|
if (!isPathInside(rootWithSep, realPath)) {
|
|
558
|
-
throw
|
|
607
|
+
throw outsideWorkspaceError();
|
|
559
608
|
}
|
|
560
609
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
561
610
|
// irreversible side effects if a symlink target changes before validation.
|
|
@@ -575,8 +624,8 @@ async function openWritableFileInRoot(root, params) {
|
|
|
575
624
|
const cleanupCreatedPath = createdForWrite && err instanceof FsSafeError;
|
|
576
625
|
const cleanupPath = realPathForCleanup ?? ioPath;
|
|
577
626
|
await handle.close().catch(() => { });
|
|
578
|
-
if (cleanupCreatedPath) {
|
|
579
|
-
await
|
|
627
|
+
if (cleanupCreatedPath && createdIdentity) {
|
|
628
|
+
await removePathIfIdentityUnchanged(cleanupPath, createdIdentity).catch(() => { });
|
|
580
629
|
}
|
|
581
630
|
throw err;
|
|
582
631
|
}
|
|
@@ -621,7 +670,11 @@ async function appendFileInRoot(root, params) {
|
|
|
621
670
|
}
|
|
622
671
|
async function removePathInRoot(root, params) {
|
|
623
672
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
624
|
-
const resolved = await
|
|
673
|
+
const resolved = await resolvePinnedPathInRoot(root, {
|
|
674
|
+
relativePath: params.relativePath,
|
|
675
|
+
denyMutations: params.denyMutations,
|
|
676
|
+
remove: true,
|
|
677
|
+
});
|
|
625
678
|
try {
|
|
626
679
|
await removePathFallback(resolved);
|
|
627
680
|
}
|
|
@@ -640,15 +693,16 @@ async function mkdirPathInRoot(root, params) {
|
|
|
640
693
|
}
|
|
641
694
|
}
|
|
642
695
|
async function writeFileInRoot(root, params) {
|
|
643
|
-
|
|
644
|
-
|
|
696
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
697
|
+
if (process.platform === "win32" &&
|
|
698
|
+
(params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
|
|
645
699
|
await writeFileFallback(root, params);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
703
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
704
|
+
await commitPinnedWriteInRoot(root, pinned, params);
|
|
646
705
|
});
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
650
|
-
await serializePathWrite(pinned.targetPath, async () => {
|
|
651
|
-
await commitPinnedWriteInRoot(root, pinned, params);
|
|
652
706
|
});
|
|
653
707
|
}
|
|
654
708
|
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
@@ -664,6 +718,7 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
664
718
|
mode: params.mode ?? pinned.mode,
|
|
665
719
|
overwrite: params.overwrite,
|
|
666
720
|
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
721
|
+
rootIdentity: root.rootIdentity,
|
|
667
722
|
});
|
|
668
723
|
}
|
|
669
724
|
catch (error) {
|
|
@@ -701,26 +756,35 @@ async function copyFileInRoot(root, params) {
|
|
|
701
756
|
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
702
757
|
}
|
|
703
758
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
await
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
759
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
760
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
761
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
762
|
+
await assertCopySourceCurrent(source);
|
|
763
|
+
let identity;
|
|
764
|
+
try {
|
|
765
|
+
identity = await runPinnedWriteHelper({
|
|
766
|
+
rootPath: pinned.rootReal,
|
|
767
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
768
|
+
basename: pinned.basename,
|
|
769
|
+
mkdir: params.mkdir !== false,
|
|
770
|
+
mode: pinned.mode,
|
|
771
|
+
overwrite: true,
|
|
772
|
+
maxBytes: params.maxBytes,
|
|
773
|
+
input: { kind: "stream", stream: source.handle.createReadStream() },
|
|
774
|
+
rootIdentity: root.rootIdentity,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
throw normalizePinnedWriteError(error);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
await assertCopySourcePathCurrent(source);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
716
787
|
});
|
|
717
|
-
try {
|
|
718
|
-
await assertCopySourcePathCurrent(source);
|
|
719
|
-
}
|
|
720
|
-
catch (error) {
|
|
721
|
-
await removeCopyTargetIfUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
722
|
-
throw error;
|
|
723
|
-
}
|
|
724
788
|
});
|
|
725
789
|
}
|
|
726
790
|
finally {
|
|
@@ -740,7 +804,7 @@ async function assertCopySourcePathCurrent(source) {
|
|
|
740
804
|
throw new FsSafeError("path-mismatch", "copy source path changed");
|
|
741
805
|
}
|
|
742
806
|
}
|
|
743
|
-
async function
|
|
807
|
+
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
744
808
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
745
809
|
const current = await fs.lstat(targetPath);
|
|
746
810
|
if (current.isSymbolicLink() || !sameFileIdentity(current, identity)) {
|
|
@@ -751,25 +815,15 @@ async function removeCopyTargetIfUnchanged(targetPath, identity) {
|
|
|
751
815
|
});
|
|
752
816
|
}
|
|
753
817
|
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
754
|
-
const { rootReal, rootWithSep, resolved } = await
|
|
755
|
-
|
|
818
|
+
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
819
|
+
relativePath,
|
|
820
|
+
denyMutations,
|
|
756
821
|
});
|
|
757
|
-
await assertMutationNotDenied(resolved, denyMutations);
|
|
758
|
-
try {
|
|
759
|
-
await assertNoPathAliasEscape({
|
|
760
|
-
absolutePath: resolved,
|
|
761
|
-
rootPath: rootReal,
|
|
762
|
-
boundaryLabel: "root",
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
catch (err) {
|
|
766
|
-
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
767
|
-
}
|
|
768
822
|
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
769
823
|
// is rejected upstream.
|
|
770
824
|
const relativeResolved = path.relative(rootReal, resolved);
|
|
771
825
|
if (path.isAbsolute(relativeResolved)) {
|
|
772
|
-
throw
|
|
826
|
+
throw outsideWorkspaceError();
|
|
773
827
|
}
|
|
774
828
|
const relativePosix = relativeResolved
|
|
775
829
|
? relativeResolved.split(path.sep).join(path.posix.sep)
|
|
@@ -784,11 +838,12 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
784
838
|
relativePath,
|
|
785
839
|
hardlinks: "reject",
|
|
786
840
|
nonBlockingRead: true,
|
|
841
|
+
symlinks: "follow-within-root",
|
|
787
842
|
});
|
|
788
843
|
try {
|
|
789
844
|
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
790
845
|
if (!isPathInside(rootWithSep, opened.realPath)) {
|
|
791
|
-
throw
|
|
846
|
+
throw outsideWorkspaceError();
|
|
792
847
|
}
|
|
793
848
|
}
|
|
794
849
|
finally {
|
|
@@ -812,17 +867,9 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
812
867
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
813
868
|
allowRoot: params.allowRoot,
|
|
814
869
|
denyMutations: params.denyMutations,
|
|
815
|
-
protectDenyMutationAncestors:
|
|
870
|
+
protectDenyMutationAncestors: params.remove === true,
|
|
816
871
|
relativePath: params.relativePath,
|
|
817
|
-
policy: PATH_ALIAS_POLICIES.strict,
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
async function resolvePinnedRemovePathInRoot(root, relativePath, denyMutations) {
|
|
821
|
-
return await resolvePinnedOperationPathInRoot(root, {
|
|
822
|
-
denyMutations,
|
|
823
|
-
protectDenyMutationAncestors: true,
|
|
824
|
-
relativePath,
|
|
825
|
-
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
872
|
+
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
826
873
|
});
|
|
827
874
|
}
|
|
828
875
|
async function resolvePinnedOperationPathInRoot(root, params) {
|
|
@@ -840,11 +887,11 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
840
887
|
relativeResolved === "." ||
|
|
841
888
|
firstSegment === ".." ||
|
|
842
889
|
path.isAbsolute(relativeResolved)) {
|
|
843
|
-
throw
|
|
890
|
+
throw outsideWorkspaceError();
|
|
844
891
|
}
|
|
845
892
|
const relativePosix = relativeResolved.split(path.sep).join(path.posix.sep);
|
|
846
893
|
if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
847
|
-
throw
|
|
894
|
+
throw outsideWorkspaceError();
|
|
848
895
|
}
|
|
849
896
|
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
|
|
850
897
|
protectAncestors: params.protectDenyMutationAncestors,
|
|
@@ -852,6 +899,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
852
899
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
853
900
|
}
|
|
854
901
|
async function resolvePinnedRootPathInRoot(root, params) {
|
|
902
|
+
await assertRootIdentityCurrent(root);
|
|
855
903
|
const rootReal = root.rootReal;
|
|
856
904
|
let resolved;
|
|
857
905
|
try {
|
|
@@ -876,11 +924,25 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
876
924
|
canonicalPath: resolved.canonicalPath,
|
|
877
925
|
};
|
|
878
926
|
}
|
|
927
|
+
async function prepareRemoveGuard(targetPath) {
|
|
928
|
+
try {
|
|
929
|
+
const guard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
930
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
|
|
931
|
+
await assertAsyncDirectoryGuard(guard);
|
|
932
|
+
return guard;
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
throw normalizeRemoveGuardError(error);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
879
938
|
async function removePathFallback(resolved) {
|
|
880
|
-
const guard = await
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
939
|
+
const guard = await prepareRemoveGuard(resolved.resolved);
|
|
940
|
+
try {
|
|
941
|
+
await ((await fs.lstat(resolved.resolved)).isDirectory() ? fs.rmdir(resolved.resolved) : fs.rm(resolved.resolved));
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
throw normalizeRemovePathError(error);
|
|
945
|
+
}
|
|
884
946
|
await assertAsyncDirectoryGuard(guard).catch(() => undefined);
|
|
885
947
|
}
|
|
886
948
|
async function mkdirPathFallback(resolved) {
|
|
@@ -892,13 +954,13 @@ async function mkdirPathFallback(resolved) {
|
|
|
892
954
|
async function statPathFallback(root, relativePath) {
|
|
893
955
|
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
894
956
|
try {
|
|
895
|
-
|
|
957
|
+
const stat = pathStatFromStats(await fs.lstat(resolved.resolved));
|
|
958
|
+
await assertRootIdentityCurrent(root);
|
|
959
|
+
return stat;
|
|
896
960
|
}
|
|
897
961
|
catch (error) {
|
|
898
962
|
if (isNotFoundPathError(error)) {
|
|
899
|
-
throw
|
|
900
|
-
cause: error instanceof Error ? error : undefined,
|
|
901
|
-
});
|
|
963
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
902
964
|
}
|
|
903
965
|
throw error;
|
|
904
966
|
}
|
|
@@ -909,6 +971,7 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
909
971
|
const names = await fs.readdir(resolved.resolved);
|
|
910
972
|
const sortedNames = names.toSorted();
|
|
911
973
|
if (!withFileTypes) {
|
|
974
|
+
await assertRootIdentityCurrent(root);
|
|
912
975
|
return sortedNames;
|
|
913
976
|
}
|
|
914
977
|
const entries = [];
|
|
@@ -918,6 +981,7 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
918
981
|
...pathStatFromStats(await fs.lstat(path.join(resolved.resolved, name))),
|
|
919
982
|
});
|
|
920
983
|
}
|
|
984
|
+
await assertRootIdentityCurrent(root);
|
|
921
985
|
return entries;
|
|
922
986
|
}
|
|
923
987
|
catch (error) {
|
|
@@ -930,6 +994,8 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
930
994
|
}
|
|
931
995
|
}
|
|
932
996
|
async function assertMoveMutationAllowed(root, params) {
|
|
997
|
+
// Keep this preflight separate from the pinned resolutions in movePathFallback:
|
|
998
|
+
// mutation denials must take precedence over source alias or identity failures.
|
|
933
999
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
934
1000
|
aliasErrorCode: "path-alias",
|
|
935
1001
|
allowFinalSymlink: true,
|
|
@@ -951,40 +1017,29 @@ async function movePathFallback(root, params) {
|
|
|
951
1017
|
relativePath: params.fromRelative,
|
|
952
1018
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
953
1019
|
});
|
|
954
|
-
const target = await
|
|
955
|
-
aliasErrorCode: "path-alias",
|
|
956
|
-
allowFinalSymlink: true,
|
|
957
|
-
});
|
|
958
|
-
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
959
|
-
await resolvePinnedRootPathInRoot(root, {
|
|
1020
|
+
const target = await resolveGuardedWritePathInRoot(root, {
|
|
960
1021
|
relativePath: params.toRelative,
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
absolutePath: target.resolved,
|
|
969
|
-
rootPath: target.rootReal,
|
|
970
|
-
boundaryLabel: "root",
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
catch (error) {
|
|
974
|
-
throw new FsSafeError("path-alias", "path alias escape blocked", {
|
|
975
|
-
cause: error instanceof Error ? error : undefined,
|
|
1022
|
+
denyMutations: params.denyMutations,
|
|
1023
|
+
allowFinalSymlink: true,
|
|
1024
|
+
protectDeniedAncestors: true,
|
|
1025
|
+
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
1026
|
+
await resolvePinnedRootPathInRoot(root, {
|
|
1027
|
+
relativePath: params.toRelative,
|
|
1028
|
+
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
976
1029
|
});
|
|
977
|
-
|
|
978
|
-
|
|
1030
|
+
const targetStat = await fs.lstat(resolvedTarget.resolved).catch(() => undefined);
|
|
1031
|
+
return !(process.platform !== "win32" &&
|
|
1032
|
+
params.overwrite &&
|
|
1033
|
+
targetStat?.isSymbolicLink() === true);
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
979
1036
|
let sourceStat;
|
|
980
1037
|
try {
|
|
981
1038
|
sourceStat = await fs.lstat(source.resolved);
|
|
982
1039
|
}
|
|
983
1040
|
catch (error) {
|
|
984
1041
|
if (isNotFoundPathError(error)) {
|
|
985
|
-
throw
|
|
986
|
-
cause: error instanceof Error ? error : undefined,
|
|
987
|
-
});
|
|
1042
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
988
1043
|
}
|
|
989
1044
|
throw error;
|
|
990
1045
|
}
|
|
@@ -992,7 +1047,7 @@ async function movePathFallback(root, params) {
|
|
|
992
1047
|
throw new FsSafeError("symlink", "symlink not allowed");
|
|
993
1048
|
}
|
|
994
1049
|
if (sourceStat.isFile() && sourceStat.nlink > 1) {
|
|
995
|
-
throw
|
|
1050
|
+
throw hardlinkedPathNotAllowedError();
|
|
996
1051
|
}
|
|
997
1052
|
if (!params.overwrite && sourceStat.isDirectory()) {
|
|
998
1053
|
throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
@@ -1021,9 +1076,7 @@ async function movePathFallback(root, params) {
|
|
|
1021
1076
|
}
|
|
1022
1077
|
catch (error) {
|
|
1023
1078
|
if (isNotFoundPathError(error)) {
|
|
1024
|
-
throw
|
|
1025
|
-
cause: error instanceof Error ? error : undefined,
|
|
1026
|
-
});
|
|
1079
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
1027
1080
|
}
|
|
1028
1081
|
if (hasNodeErrorCode(error, "EEXIST")) {
|
|
1029
1082
|
throw new FsSafeError("already-exists", "destination exists", {
|
|
@@ -1089,30 +1142,22 @@ async function writeFileFallback(root, params) {
|
|
|
1089
1142
|
}
|
|
1090
1143
|
}
|
|
1091
1144
|
async function writeMissingFileFallback(root, params) {
|
|
1092
|
-
const { rootReal, resolved } = await
|
|
1093
|
-
|
|
1145
|
+
const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
1146
|
+
relativePath: params.relativePath,
|
|
1147
|
+
denyMutations: params.denyMutations,
|
|
1094
1148
|
});
|
|
1095
|
-
await assertMutationNotDenied(resolved, params.denyMutations);
|
|
1096
|
-
try {
|
|
1097
|
-
await assertNoPathAliasEscape({
|
|
1098
|
-
absolutePath: resolved,
|
|
1099
|
-
rootPath: rootReal,
|
|
1100
|
-
boundaryLabel: "root",
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
catch (err) {
|
|
1104
|
-
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
1105
|
-
}
|
|
1106
1149
|
const targetPath = params.mkdir === false
|
|
1107
1150
|
? resolved
|
|
1108
1151
|
: await prepareRootWriteTarget(rootReal, resolved);
|
|
1109
1152
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
1110
1153
|
let created = false;
|
|
1154
|
+
let createdIdentity;
|
|
1111
1155
|
try {
|
|
1112
1156
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1113
1157
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
|
|
1114
1158
|
created = true;
|
|
1115
1159
|
try {
|
|
1160
|
+
createdIdentity = await handle.stat();
|
|
1116
1161
|
if (typeof params.data === "string") {
|
|
1117
1162
|
await handle.writeFile(params.data, params.encoding ?? "utf8");
|
|
1118
1163
|
}
|
|
@@ -1148,8 +1193,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1148
1193
|
throw err;
|
|
1149
1194
|
}
|
|
1150
1195
|
finally {
|
|
1151
|
-
if (created) {
|
|
1152
|
-
await
|
|
1196
|
+
if (created && createdIdentity) {
|
|
1197
|
+
await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
|
|
1153
1198
|
}
|
|
1154
1199
|
}
|
|
1155
1200
|
}
|