@openclaw/fs-safe 0.1.1 → 0.2.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 +58 -0
- package/README.md +58 -2
- package/dist/absolute-path.d.ts +14 -0
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +203 -2
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +81 -7
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +25 -42
- package/dist/bounded-read-stream.d.ts +8 -0
- package/dist/bounded-read-stream.d.ts.map +1 -0
- package/dist/bounded-read-stream.js +20 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/directory-guard.d.ts +18 -0
- package/dist/directory-guard.d.ts.map +1 -0
- package/dist/directory-guard.js +70 -0
- package/dist/file-lock.d.ts +4 -2
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +13 -2
- package/dist/file-store-boundary.d.ts +25 -0
- package/dist/file-store-boundary.d.ts.map +1 -0
- package/dist/file-store-boundary.js +168 -0
- package/dist/file-store-prune.d.ts +12 -0
- package/dist/file-store-prune.d.ts.map +1 -0
- package/dist/file-store-prune.js +86 -0
- package/dist/file-store-source.d.ts +5 -0
- package/dist/file-store-source.d.ts.map +1 -0
- package/dist/file-store-source.js +30 -0
- package/dist/file-store.d.ts +2 -6
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +122 -125
- package/dist/guarded-mkdir.d.ts +6 -0
- package/dist/guarded-mkdir.d.ts.map +1 -0
- package/dist/guarded-mkdir.js +45 -0
- package/dist/guarded-mutation.d.ts +33 -0
- package/dist/guarded-mutation.d.ts.map +1 -0
- package/dist/guarded-mutation.js +76 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/json-document-store.d.ts +2 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +7 -3
- package/dist/json-durable-queue.d.ts +45 -0
- package/dist/json-durable-queue.d.ts.map +1 -0
- package/dist/json-durable-queue.js +200 -0
- package/dist/json.d.ts +35 -5
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +69 -2
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +17 -3
- package/dist/lock-config.d.ts +10 -0
- package/dist/lock-config.d.ts.map +1 -0
- package/dist/lock-config.js +12 -0
- package/dist/move-path.d.ts +1 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +195 -7
- package/dist/output.d.ts +13 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +69 -0
- package/dist/path-stat.d.ts +4 -0
- package/dist/path-stat.d.ts.map +1 -0
- package/dist/path-stat.js +15 -0
- package/dist/path.d.ts +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +6 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +44 -19
- package/dist/private-file-store.d.ts +5 -7
- package/dist/private-file-store.d.ts.map +1 -1
- package/dist/private-file-store.js +21 -34
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +28 -15
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +52 -8
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +5 -4
- package/dist/replace-file.d.ts +1 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +93 -31
- package/dist/root-context.d.ts +23 -0
- package/dist/root-context.d.ts.map +1 -0
- package/dist/root-context.js +62 -0
- package/dist/root-errors.d.ts +4 -0
- package/dist/root-errors.d.ts.map +1 -0
- package/dist/root-errors.js +21 -0
- package/dist/root-impl.d.ts +108 -0
- package/dist/root-impl.d.ts.map +1 -0
- package/dist/{safe-root.js → root-impl.js} +484 -265
- package/dist/root.d.ts +1 -107
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +1 -1332
- package/dist/safe-path-segment.d.ts +9 -0
- package/dist/safe-path-segment.d.ts.map +1 -0
- package/dist/safe-path-segment.js +51 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -1
- package/dist/sibling-temp.d.ts +1 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +32 -7
- package/dist/sidecar-lock.d.ts +2 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +93 -13
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +1 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +7 -4
- package/dist/test-hooks.d.ts +6 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -2
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +48 -13
- package/docs/advanced.md +110 -0
- package/docs/archive.md +199 -0
- package/docs/atomic.md +191 -0
- package/docs/config.md +94 -0
- package/docs/contributing.md +80 -0
- package/docs/errors.md +154 -0
- package/docs/file-store.md +213 -0
- package/docs/filename.md +0 -0
- package/docs/index.md +77 -0
- package/docs/install-path.md +167 -0
- package/docs/install.md +150 -0
- package/docs/json-store.md +204 -0
- package/docs/json.md +192 -0
- package/docs/local-roots.md +164 -0
- package/docs/output.md +92 -0
- package/docs/path-scope.md +130 -0
- package/docs/path.md +191 -0
- package/docs/permissions.md +95 -0
- package/docs/private-file-store.md +45 -0
- package/docs/python-helper.md +103 -0
- package/docs/quickstart.md +147 -0
- package/docs/reading.md +174 -0
- package/docs/regular-file.md +175 -0
- package/docs/root.md +205 -0
- package/docs/secret-file.md +154 -0
- package/docs/secure-file.md +82 -0
- package/docs/security-model.md +99 -0
- package/docs/sidecar-lock.md +230 -0
- package/docs/store.md +77 -0
- package/docs/temp.md +284 -0
- package/docs/test-hooks.md +74 -0
- package/docs/testing.md +178 -0
- package/docs/timing.md +121 -0
- package/docs/types.md +155 -0
- package/docs/walk.md +63 -0
- package/docs/writing.md +222 -0
- package/package.json +10 -2
- package/dist/archive-utils.d.ts +0 -3
- package/dist/archive-utils.d.ts.map +0 -1
- package/dist/archive-utils.js +0 -26
- package/dist/boundary-file-read.d.ts +0 -44
- package/dist/boundary-file-read.d.ts.map +0 -1
- package/dist/boundary-file-read.js +0 -129
- package/dist/boundary-path.d.ts +0 -39
- package/dist/boundary-path.d.ts.map +0 -1
- package/dist/boundary-path.js +0 -598
- package/dist/fs-pinned-path-helper.d.ts +0 -7
- package/dist/fs-pinned-path-helper.d.ts.map +0 -1
- package/dist/fs-pinned-path-helper.js +0 -182
- package/dist/fs-pinned-write-helper.d.ts +0 -21
- package/dist/fs-pinned-write-helper.d.ts.map +0 -1
- package/dist/fs-pinned-write-helper.js +0 -263
- package/dist/hardlink-guards.d.ts +0 -7
- package/dist/hardlink-guards.d.ts.map +0 -1
- package/dist/hardlink-guards.js +0 -30
- package/dist/install-safe-path.d.ts +0 -20
- package/dist/install-safe-path.d.ts.map +0 -1
- package/dist/install-safe-path.js +0 -94
- package/dist/json-file.d.ts +0 -3
- package/dist/json-file.d.ts.map +0 -1
- package/dist/json-file.js +0 -123
- package/dist/json-files.d.ts +0 -20
- package/dist/json-files.d.ts.map +0 -1
- package/dist/json-files.js +0 -153
- package/dist/path-alias-guards.d.ts +0 -19
- package/dist/path-alias-guards.d.ts.map +0 -1
- package/dist/path-alias-guards.js +0 -21
- package/dist/path-guards.d.ts +0 -7
- package/dist/path-guards.d.ts.map +0 -1
- package/dist/path-guards.js +0 -49
- package/dist/path-safety.d.ts +0 -12
- package/dist/path-safety.d.ts.map +0 -1
- package/dist/path-safety.js +0 -50
- package/dist/safe-open-sync.d.ts +0 -24
- package/dist/safe-open-sync.d.ts.map +0 -1
- package/dist/safe-open-sync.js +0 -71
- package/dist/safe-root.d.ts +0 -123
- package/dist/safe-root.d.ts.map +0 -1
- package/dist/secure-temp-workspace.d.ts +0 -25
- package/dist/secure-temp-workspace.d.ts.map +0 -1
- package/dist/secure-temp-workspace.js +0 -136
- package/dist/sibling-temp-file.d.ts +0 -16
- package/dist/sibling-temp-file.d.ts.map +0 -1
- package/dist/sibling-temp-file.js +0 -73
- package/dist/sibling-temp-write.d.ts +0 -8
- package/dist/sibling-temp-write.d.ts.map +0 -1
- package/dist/sibling-temp-write.js +0 -40
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { createBoundedReadStream } from "./bounded-read-stream.js";
|
|
7
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
7
8
|
import { FsSafeError } from "./errors.js";
|
|
8
9
|
import { sameFileIdentity } from "./file-identity.js";
|
|
10
|
+
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
11
|
+
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
9
12
|
import { isPinnedPathHelperSpawnError, runPinnedPathHelper } from "./pinned-path.js";
|
|
10
|
-
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
11
|
-
import {
|
|
13
|
+
import { runPinnedCopyHelper, runPinnedWriteHelper } from "./pinned-write.js";
|
|
14
|
+
import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
12
15
|
import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
13
|
-
import { hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
14
|
-
import { helperReaddir, helperStat, runPinnedHelper } from "./pinned-helper.js";
|
|
16
|
+
import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
17
|
+
import { helperReaddir, helperStat, runPinnedHelper, } from "./pinned-helper.js";
|
|
18
|
+
import { pathStatFromStats } from "./path-stat.js";
|
|
15
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";
|
|
16
22
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
23
|
+
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
24
|
+
import { serializePathWrite } from "./write-queue.js";
|
|
17
25
|
function logWarn(message) {
|
|
18
26
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
19
27
|
console.warn(message);
|
|
@@ -36,21 +44,19 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
36
44
|
fsConstants.O_CREAT |
|
|
37
45
|
fsConstants.O_EXCL |
|
|
38
46
|
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return expandHomePrefix(relativePath, { home: cachedHomePath.real });
|
|
47
|
+
export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
48
|
+
function closeHandleForDispose(handle) {
|
|
49
|
+
return handle.close().catch(() => undefined);
|
|
50
|
+
}
|
|
51
|
+
function openResult(params) {
|
|
52
|
+
return {
|
|
53
|
+
handle: params.handle,
|
|
54
|
+
realPath: params.realPath,
|
|
55
|
+
stat: params.stat,
|
|
56
|
+
[Symbol.asyncDispose]: async () => {
|
|
57
|
+
await closeHandleForDispose(params.handle);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
54
60
|
}
|
|
55
61
|
async function openVerifiedLocalFile(filePath, options) {
|
|
56
62
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
@@ -71,7 +77,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
71
77
|
}
|
|
72
78
|
let handle;
|
|
73
79
|
try {
|
|
74
|
-
const openFlags = options?.
|
|
80
|
+
const openFlags = options?.symlinks === "follow-within-root"
|
|
75
81
|
? options?.nonBlockingRead
|
|
76
82
|
? OPEN_READ_FOLLOW_NONBLOCK_FLAGS
|
|
77
83
|
: OPEN_READ_FOLLOW_FLAGS
|
|
@@ -106,10 +112,10 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
106
112
|
if (!stat.isFile()) {
|
|
107
113
|
throw new FsSafeError("not-file", "not a file");
|
|
108
114
|
}
|
|
109
|
-
if (options?.
|
|
115
|
+
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
110
116
|
throw new FsSafeError("hardlink", "hardlinked path not allowed");
|
|
111
117
|
}
|
|
112
|
-
if (options?.
|
|
118
|
+
if (options?.symlinks === "follow-within-root") {
|
|
113
119
|
const pathStat = await fs.stat(filePath);
|
|
114
120
|
if (!sameFileIdentity(stat, pathStat)) {
|
|
115
121
|
throw new FsSafeError("path-mismatch", "path changed during read");
|
|
@@ -126,13 +132,13 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
126
132
|
}
|
|
127
133
|
const realPath = await resolveOpenedFileRealPathForHandle(handle, filePath);
|
|
128
134
|
const realStat = await fs.stat(realPath);
|
|
129
|
-
if (options?.
|
|
135
|
+
if (options?.hardlinks === "reject" && realStat.nlink > 1) {
|
|
130
136
|
throw new FsSafeError("hardlink", "hardlinked path not allowed");
|
|
131
137
|
}
|
|
132
138
|
if (!sameFileIdentity(stat, realStat)) {
|
|
133
139
|
throw new FsSafeError("path-mismatch", "path mismatch");
|
|
134
140
|
}
|
|
135
|
-
return { handle, realPath, stat };
|
|
141
|
+
return openResult({ handle, realPath, stat });
|
|
136
142
|
}
|
|
137
143
|
catch (err) {
|
|
138
144
|
await handle.close().catch(() => { });
|
|
@@ -145,35 +151,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
145
151
|
throw err;
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
let rootReal;
|
|
150
|
-
try {
|
|
151
|
-
rootReal = await fs.realpath(rootDir);
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
if (isNotFoundPathError(err)) {
|
|
155
|
-
throw new FsSafeError("not-found", "root dir not found");
|
|
156
|
-
}
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
rootDir: path.resolve(rootDir),
|
|
161
|
-
rootReal,
|
|
162
|
-
rootWithSep: ensureTrailingSep(rootReal),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
async function resolvePathInSafeRoot(root, relativePath) {
|
|
166
|
-
const expanded = await expandRelativePathWithHome(relativePath);
|
|
167
|
-
const resolved = path.resolve(root.rootWithSep, expanded);
|
|
168
|
-
if (!isPathInside(root.rootWithSep, resolved)) {
|
|
169
|
-
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
170
|
-
}
|
|
171
|
-
return { rootReal: root.rootReal, rootWithSep: root.rootWithSep, resolved };
|
|
172
|
-
}
|
|
173
|
-
async function resolvePathWithinRoot(params) {
|
|
174
|
-
return await resolvePathInSafeRoot(await resolveSafeRootContext(params.rootDir), params.relativePath);
|
|
175
|
-
}
|
|
176
|
-
export class SafeRoot {
|
|
154
|
+
class RootHandle {
|
|
177
155
|
rootDir;
|
|
178
156
|
rootReal;
|
|
179
157
|
rootWithSep;
|
|
@@ -192,75 +170,90 @@ export class SafeRoot {
|
|
|
192
170
|
};
|
|
193
171
|
}
|
|
194
172
|
async resolve(relativePath) {
|
|
195
|
-
return (await
|
|
173
|
+
return (await resolvePathInRoot(this.context, relativePath)).resolved;
|
|
196
174
|
}
|
|
197
|
-
async
|
|
198
|
-
return await
|
|
175
|
+
async open(relativePath, options = {}) {
|
|
176
|
+
return await openFileInRoot(this.context, {
|
|
199
177
|
relativePath,
|
|
200
|
-
...this.defaults
|
|
201
|
-
...this.defaults.open,
|
|
178
|
+
...readDefaults(this.defaults),
|
|
202
179
|
...options,
|
|
203
180
|
});
|
|
204
181
|
}
|
|
205
|
-
async
|
|
206
|
-
return await
|
|
182
|
+
async read(relativePath, options = {}) {
|
|
183
|
+
return await readFileInRoot(this.context, {
|
|
207
184
|
relativePath,
|
|
208
|
-
...this.defaults
|
|
185
|
+
...readDefaults(this.defaults),
|
|
209
186
|
...options,
|
|
210
187
|
});
|
|
211
188
|
}
|
|
212
|
-
async
|
|
213
|
-
return (await this.
|
|
189
|
+
async readBytes(relativePath, options = {}) {
|
|
190
|
+
return (await this.read(relativePath, options)).buffer;
|
|
214
191
|
}
|
|
215
192
|
async readText(relativePath, options = {}) {
|
|
216
193
|
const { encoding = "utf8", ...readOptions } = options;
|
|
217
|
-
return (await this.
|
|
194
|
+
return (await this.read(relativePath, readOptions)).buffer.toString(encoding);
|
|
218
195
|
}
|
|
219
196
|
async readJson(relativePath, options = {}) {
|
|
220
197
|
return JSON.parse(await this.readText(relativePath, options));
|
|
221
198
|
}
|
|
222
|
-
async
|
|
223
|
-
return await
|
|
199
|
+
async readAbsolute(filePath, options = {}) {
|
|
200
|
+
return await readPathInRoot(this.context, {
|
|
201
|
+
filePath,
|
|
202
|
+
...readDefaults(this.defaults),
|
|
203
|
+
...options,
|
|
204
|
+
});
|
|
224
205
|
}
|
|
225
206
|
reader(options = {}) {
|
|
226
207
|
return async (filePath) => {
|
|
227
|
-
return (await this.
|
|
208
|
+
return (await this.readAbsolute(filePath, options)).buffer;
|
|
228
209
|
};
|
|
229
210
|
}
|
|
230
211
|
async openWritable(relativePath, options = {}) {
|
|
231
|
-
|
|
212
|
+
const writeMode = options.writeMode ?? "replace";
|
|
213
|
+
return await openWritableFileInRoot(this.context, {
|
|
232
214
|
relativePath,
|
|
233
|
-
|
|
215
|
+
mkdir: this.defaults.mkdir,
|
|
216
|
+
mode: this.defaults.mode,
|
|
234
217
|
...options,
|
|
218
|
+
append: writeMode === "append",
|
|
219
|
+
truncateExisting: writeMode === "replace",
|
|
235
220
|
});
|
|
236
221
|
}
|
|
237
222
|
async append(relativePath, data, options = {}) {
|
|
238
|
-
await
|
|
223
|
+
await appendFileInRoot(this.context, {
|
|
239
224
|
relativePath,
|
|
240
225
|
data,
|
|
241
|
-
|
|
226
|
+
mkdir: this.defaults.mkdir,
|
|
227
|
+
mode: this.defaults.mode,
|
|
242
228
|
...options,
|
|
243
229
|
});
|
|
244
230
|
}
|
|
245
|
-
async
|
|
246
|
-
|
|
231
|
+
async remove(relativePath) {
|
|
232
|
+
assertValidRootRelativePath(relativePath);
|
|
233
|
+
await removePathInRoot(this.context, relativePath);
|
|
234
|
+
}
|
|
235
|
+
async mkdir(relativePath) {
|
|
236
|
+
assertValidRootRelativePath(relativePath);
|
|
237
|
+
await mkdirPathInRoot(this.context, { relativePath });
|
|
247
238
|
}
|
|
248
|
-
async
|
|
249
|
-
await
|
|
239
|
+
async ensureRoot() {
|
|
240
|
+
await mkdirPathInRoot(this.context, { relativePath: "", allowRoot: true });
|
|
250
241
|
}
|
|
251
|
-
async
|
|
252
|
-
await
|
|
242
|
+
async write(relativePath, data, options = {}) {
|
|
243
|
+
await writeFileInRoot(this.context, {
|
|
253
244
|
relativePath,
|
|
254
245
|
data,
|
|
255
|
-
|
|
246
|
+
mkdir: this.defaults.mkdir,
|
|
247
|
+
mode: this.defaults.mode,
|
|
256
248
|
...options,
|
|
257
249
|
});
|
|
258
250
|
}
|
|
259
|
-
async
|
|
260
|
-
|
|
251
|
+
async create(relativePath, data, options = {}) {
|
|
252
|
+
await writeFileInRoot(this.context, {
|
|
261
253
|
relativePath,
|
|
262
254
|
data,
|
|
263
|
-
|
|
255
|
+
mkdir: this.defaults.mkdir,
|
|
256
|
+
mode: this.defaults.mode,
|
|
264
257
|
...options,
|
|
265
258
|
overwrite: false,
|
|
266
259
|
});
|
|
@@ -268,47 +261,103 @@ export class SafeRoot {
|
|
|
268
261
|
async writeJson(relativePath, data, options = {}) {
|
|
269
262
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
270
263
|
const json = JSON.stringify(data, replacer, space);
|
|
271
|
-
await this.
|
|
264
|
+
await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
272
265
|
}
|
|
273
266
|
async createJson(relativePath, data, options = {}) {
|
|
274
267
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
275
268
|
const json = JSON.stringify(data, replacer, space);
|
|
276
|
-
|
|
269
|
+
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
277
270
|
}
|
|
278
|
-
async
|
|
279
|
-
|
|
271
|
+
async copyIn(relativePath, sourcePath, options = {}) {
|
|
272
|
+
assertValidRootRelativePath(relativePath);
|
|
273
|
+
await copyFileInRoot(this.context, {
|
|
280
274
|
sourcePath,
|
|
281
275
|
relativePath,
|
|
282
|
-
|
|
276
|
+
maxBytes: this.defaults.maxBytes,
|
|
277
|
+
mkdir: this.defaults.mkdir,
|
|
278
|
+
mode: this.defaults.mode,
|
|
283
279
|
...options,
|
|
284
280
|
});
|
|
285
281
|
}
|
|
286
|
-
async
|
|
287
|
-
|
|
282
|
+
async exists(relativePath) {
|
|
283
|
+
try {
|
|
284
|
+
await this.stat(relativePath);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
if (err instanceof FsSafeError && err.code === "not-found") {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
288
293
|
}
|
|
289
|
-
async
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
async stat(relativePath) {
|
|
295
|
+
assertValidRootRelativePath(relativePath);
|
|
296
|
+
try {
|
|
297
|
+
return await helperStat(this.rootReal, relativePath);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (canFallbackFromPythonError(error)) {
|
|
301
|
+
return await statPathFallback(this.context, relativePath);
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
293
305
|
}
|
|
294
|
-
async
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
306
|
+
async list(relativePath, options = {}) {
|
|
307
|
+
assertValidRootRelativePath(relativePath);
|
|
308
|
+
try {
|
|
309
|
+
return options.withFileTypes === true
|
|
310
|
+
? await helperReaddir(this.rootReal, relativePath, true)
|
|
311
|
+
: await helperReaddir(this.rootReal, relativePath, false);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (canFallbackFromPythonError(error)) {
|
|
315
|
+
return await listPathFallback(this.context, relativePath, options.withFileTypes === true);
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async move(fromRelative, toRelative, options = {}) {
|
|
321
|
+
assertValidRootRelativePath(fromRelative);
|
|
322
|
+
assertValidRootRelativePath(toRelative);
|
|
323
|
+
try {
|
|
324
|
+
await runPinnedHelper("rename", this.rootReal, {
|
|
325
|
+
from: fromRelative,
|
|
326
|
+
overwrite: options.overwrite ?? false,
|
|
327
|
+
to: toRelative,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (canFallbackFromPythonError(error)) {
|
|
332
|
+
await movePathFallback(this.context, {
|
|
333
|
+
fromRelative,
|
|
334
|
+
overwrite: options.overwrite ?? false,
|
|
335
|
+
toRelative,
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
300
341
|
}
|
|
301
342
|
}
|
|
302
|
-
|
|
303
|
-
return
|
|
343
|
+
function readDefaults(defaults) {
|
|
344
|
+
return {
|
|
345
|
+
hardlinks: defaults.hardlinks,
|
|
346
|
+
maxBytes: defaults.maxBytes ?? DEFAULT_ROOT_MAX_BYTES,
|
|
347
|
+
nonBlockingRead: defaults.nonBlockingRead,
|
|
348
|
+
symlinks: defaults.symlinks,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export async function root(rootDir, defaults = {}) {
|
|
352
|
+
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
304
353
|
}
|
|
305
|
-
async function
|
|
306
|
-
const { rootWithSep, resolved } = await
|
|
354
|
+
async function openFileInRoot(root, params) {
|
|
355
|
+
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
307
356
|
let opened;
|
|
308
357
|
try {
|
|
309
358
|
opened = await openVerifiedLocalFile(resolved, {
|
|
310
359
|
nonBlockingRead: params.nonBlockingRead,
|
|
311
|
-
|
|
360
|
+
symlinks: params.symlinks,
|
|
312
361
|
});
|
|
313
362
|
}
|
|
314
363
|
catch (err) {
|
|
@@ -317,7 +366,7 @@ async function openFileInSafeRoot(root, params) {
|
|
|
317
366
|
}
|
|
318
367
|
throw err;
|
|
319
368
|
}
|
|
320
|
-
if (params.
|
|
369
|
+
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
321
370
|
await opened.handle.close().catch(() => { });
|
|
322
371
|
throw new FsSafeError("hardlink", "hardlinked path not allowed");
|
|
323
372
|
}
|
|
@@ -327,8 +376,8 @@ async function openFileInSafeRoot(root, params) {
|
|
|
327
376
|
}
|
|
328
377
|
return opened;
|
|
329
378
|
}
|
|
330
|
-
async function
|
|
331
|
-
const opened = await
|
|
379
|
+
async function readFileInRoot(root, params) {
|
|
380
|
+
const opened = await openFileInRoot(root, params);
|
|
332
381
|
try {
|
|
333
382
|
return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes });
|
|
334
383
|
}
|
|
@@ -336,16 +385,18 @@ async function readFileInSafeRoot(root, params) {
|
|
|
336
385
|
await opened.handle.close().catch(() => { });
|
|
337
386
|
}
|
|
338
387
|
}
|
|
339
|
-
async function
|
|
388
|
+
async function readPathInRoot(root, params) {
|
|
340
389
|
const rootDir = root.rootDir;
|
|
341
390
|
const candidatePath = path.isAbsolute(params.filePath)
|
|
342
391
|
? path.resolve(params.filePath)
|
|
343
392
|
: path.resolve(rootDir, params.filePath);
|
|
344
393
|
const relativePath = path.relative(rootDir, candidatePath);
|
|
345
|
-
return await
|
|
394
|
+
return await readFileInRoot(root, {
|
|
346
395
|
relativePath,
|
|
347
|
-
|
|
396
|
+
hardlinks: params.hardlinks,
|
|
348
397
|
maxBytes: params.maxBytes,
|
|
398
|
+
nonBlockingRead: params.nonBlockingRead,
|
|
399
|
+
symlinks: params.symlinks,
|
|
349
400
|
});
|
|
350
401
|
}
|
|
351
402
|
export async function readLocalFileSafely(params) {
|
|
@@ -358,6 +409,7 @@ export async function readLocalFileSafely(params) {
|
|
|
358
409
|
}
|
|
359
410
|
}
|
|
360
411
|
export async function openLocalFileSafely(params) {
|
|
412
|
+
assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
|
|
361
413
|
return await openVerifiedLocalFile(params.filePath);
|
|
362
414
|
}
|
|
363
415
|
async function readOpenedFileSafely(params) {
|
|
@@ -365,6 +417,9 @@ async function readOpenedFileSafely(params) {
|
|
|
365
417
|
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
|
|
366
418
|
}
|
|
367
419
|
const buffer = await params.opened.handle.readFile();
|
|
420
|
+
if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
|
|
421
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
|
|
422
|
+
}
|
|
368
423
|
return {
|
|
369
424
|
buffer,
|
|
370
425
|
realPath: params.opened.realPath,
|
|
@@ -379,6 +434,9 @@ function buildAtomicWriteTempPath(targetPath) {
|
|
|
379
434
|
const base = path.basename(targetPath);
|
|
380
435
|
return path.join(dir, `.${base}.${process.pid}.${randomUUID()}.tmp`);
|
|
381
436
|
}
|
|
437
|
+
function rootWriteQueueKey(root, relativePath) {
|
|
438
|
+
return `${root.rootReal}\0${relativePath}`;
|
|
439
|
+
}
|
|
382
440
|
async function writeTempFileForAtomicReplace(params) {
|
|
383
441
|
const tempHandle = await fs.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
|
|
384
442
|
try {
|
|
@@ -395,7 +453,7 @@ async function writeTempFileForAtomicReplace(params) {
|
|
|
395
453
|
}
|
|
396
454
|
}
|
|
397
455
|
async function verifyAtomicWriteResult(params) {
|
|
398
|
-
const opened = await openVerifiedLocalFile(params.targetPath, {
|
|
456
|
+
const opened = await openVerifiedLocalFile(params.targetPath, { hardlinks: "reject" });
|
|
399
457
|
try {
|
|
400
458
|
if (!sameFileIdentity(opened.stat, params.expectedIdentity)) {
|
|
401
459
|
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
@@ -482,8 +540,8 @@ async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
|
|
|
482
540
|
}
|
|
483
541
|
return null;
|
|
484
542
|
}
|
|
485
|
-
async function
|
|
486
|
-
const { rootReal, rootWithSep, resolved } = await
|
|
543
|
+
async function openWritableFileInRoot(root, params) {
|
|
544
|
+
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
487
545
|
try {
|
|
488
546
|
await assertNoPathAliasEscape({
|
|
489
547
|
absolutePath: resolved,
|
|
@@ -495,7 +553,10 @@ async function openWritableFileInSafeRoot(root, params) {
|
|
|
495
553
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
496
554
|
}
|
|
497
555
|
if (params.mkdir !== false) {
|
|
498
|
-
await
|
|
556
|
+
const parentGuard = await createNearestExistingDirectoryGuard(rootReal, path.dirname(resolved));
|
|
557
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
558
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
559
|
+
});
|
|
499
560
|
}
|
|
500
561
|
let ioPath = resolved;
|
|
501
562
|
try {
|
|
@@ -513,20 +574,20 @@ async function openWritableFileInSafeRoot(root, params) {
|
|
|
513
574
|
throw err;
|
|
514
575
|
}
|
|
515
576
|
}
|
|
516
|
-
const
|
|
577
|
+
const mode = params.mode ?? 0o600;
|
|
517
578
|
let handle;
|
|
518
579
|
let createdForWrite = false;
|
|
519
580
|
const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
|
|
520
581
|
const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
|
|
521
582
|
try {
|
|
522
583
|
try {
|
|
523
|
-
handle = await fs.open(ioPath, existingFlags,
|
|
584
|
+
handle = await fs.open(ioPath, existingFlags, mode);
|
|
524
585
|
}
|
|
525
586
|
catch (err) {
|
|
526
587
|
if (!isNotFoundPathError(err)) {
|
|
527
588
|
throw err;
|
|
528
589
|
}
|
|
529
|
-
handle = await fs.open(ioPath, createFlags,
|
|
590
|
+
handle = await fs.open(ioPath, createFlags, mode);
|
|
530
591
|
createdForWrite = true;
|
|
531
592
|
}
|
|
532
593
|
}
|
|
@@ -537,6 +598,9 @@ async function openWritableFileInSafeRoot(root, params) {
|
|
|
537
598
|
if (isSymlinkOpenError(err)) {
|
|
538
599
|
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
539
600
|
}
|
|
601
|
+
if (hasNodeErrorCode(err, "EISDIR")) {
|
|
602
|
+
throw new FsSafeError("not-file", "not a file", { cause: err });
|
|
603
|
+
}
|
|
540
604
|
throw err;
|
|
541
605
|
}
|
|
542
606
|
let realPathForCleanup = null;
|
|
@@ -584,6 +648,9 @@ async function openWritableFileInSafeRoot(root, params) {
|
|
|
584
648
|
createdForWrite,
|
|
585
649
|
realPath,
|
|
586
650
|
stat,
|
|
651
|
+
[Symbol.asyncDispose]: async () => {
|
|
652
|
+
await closeHandleForDispose(handle);
|
|
653
|
+
},
|
|
587
654
|
};
|
|
588
655
|
}
|
|
589
656
|
catch (err) {
|
|
@@ -596,10 +663,11 @@ async function openWritableFileInSafeRoot(root, params) {
|
|
|
596
663
|
throw err;
|
|
597
664
|
}
|
|
598
665
|
}
|
|
599
|
-
async function
|
|
600
|
-
const target = await
|
|
666
|
+
async function appendFileInRoot(root, params) {
|
|
667
|
+
const target = await openWritableFileInRoot(root, {
|
|
601
668
|
relativePath: params.relativePath,
|
|
602
669
|
mkdir: params.mkdir,
|
|
670
|
+
mode: params.mode,
|
|
603
671
|
truncateExisting: false,
|
|
604
672
|
append: true,
|
|
605
673
|
});
|
|
@@ -627,8 +695,8 @@ async function appendFileInSafeRoot(root, params) {
|
|
|
627
695
|
await target.handle.close().catch(() => { });
|
|
628
696
|
}
|
|
629
697
|
}
|
|
630
|
-
async function
|
|
631
|
-
const resolved = await
|
|
698
|
+
async function removePathInRoot(root, relativePath) {
|
|
699
|
+
const resolved = await resolvePinnedRemovePathInRoot(root, relativePath);
|
|
632
700
|
if (process.platform === "win32") {
|
|
633
701
|
await removePathFallback(resolved);
|
|
634
702
|
return;
|
|
@@ -648,8 +716,8 @@ async function removePathInSafeRoot(root, relativePath) {
|
|
|
648
716
|
throw normalizePinnedPathError(error);
|
|
649
717
|
}
|
|
650
718
|
}
|
|
651
|
-
async function
|
|
652
|
-
const resolved = await
|
|
719
|
+
async function mkdirPathInRoot(root, params) {
|
|
720
|
+
const resolved = await resolvePinnedPathInRoot(root, params);
|
|
653
721
|
if (process.platform === "win32") {
|
|
654
722
|
await mkdirPathFallback(resolved);
|
|
655
723
|
return;
|
|
@@ -669,49 +737,57 @@ async function mkdirPathInSafeRoot(root, params) {
|
|
|
669
737
|
throw normalizePinnedPathError(error);
|
|
670
738
|
}
|
|
671
739
|
}
|
|
672
|
-
async function
|
|
740
|
+
async function writeFileInRoot(root, params) {
|
|
673
741
|
if (process.platform === "win32") {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const pinned = await resolvePinnedWriteTargetInSafeRoot(root, params.relativePath);
|
|
677
|
-
let identity;
|
|
678
|
-
try {
|
|
679
|
-
identity = await runPinnedWriteHelper({
|
|
680
|
-
rootPath: pinned.rootReal,
|
|
681
|
-
relativeParentPath: pinned.relativeParentPath,
|
|
682
|
-
basename: pinned.basename,
|
|
683
|
-
mkdir: params.mkdir !== false,
|
|
684
|
-
mode: pinned.mode,
|
|
685
|
-
overwrite: params.overwrite,
|
|
686
|
-
input: {
|
|
687
|
-
kind: "buffer",
|
|
688
|
-
data: params.data,
|
|
689
|
-
encoding: params.encoding,
|
|
690
|
-
},
|
|
742
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
743
|
+
await writeFileFallback(root, params);
|
|
691
744
|
});
|
|
745
|
+
return;
|
|
692
746
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
747
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
748
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
749
|
+
let identity;
|
|
750
|
+
try {
|
|
751
|
+
identity = await runPinnedWriteHelper({
|
|
752
|
+
rootPath: pinned.rootReal,
|
|
753
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
754
|
+
basename: pinned.basename,
|
|
755
|
+
mkdir: params.mkdir !== false,
|
|
756
|
+
mode: params.mode ?? pinned.mode,
|
|
757
|
+
overwrite: params.overwrite,
|
|
758
|
+
input: {
|
|
759
|
+
kind: "buffer",
|
|
760
|
+
data: params.data,
|
|
761
|
+
encoding: params.encoding,
|
|
762
|
+
},
|
|
763
|
+
});
|
|
696
764
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (params.overwrite === false && isAlreadyExistsError(error)) {
|
|
767
|
+
throw new FsSafeError("already-exists", "file already exists", {
|
|
768
|
+
cause: error instanceof Error ? error : undefined,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
throw normalizePinnedWriteError(error);
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
await verifyAtomicWriteResult({
|
|
775
|
+
root,
|
|
776
|
+
targetPath: pinned.targetPath,
|
|
777
|
+
expectedIdentity: identity,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
782
|
+
throw err;
|
|
783
|
+
}
|
|
784
|
+
});
|
|
711
785
|
}
|
|
712
|
-
async function
|
|
786
|
+
async function copyFileInRoot(root, params) {
|
|
787
|
+
assertValidRootRelativePath(params.relativePath);
|
|
788
|
+
assertNoNulPathInput(params.sourcePath, "source path contains a NUL byte");
|
|
713
789
|
const source = await openVerifiedLocalFile(params.sourcePath, {
|
|
714
|
-
|
|
790
|
+
hardlinks: params.sourceHardlinks,
|
|
715
791
|
});
|
|
716
792
|
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
717
793
|
await source.handle.close().catch(() => { });
|
|
@@ -719,43 +795,57 @@ async function copyFileInSafeRoot(root, params) {
|
|
|
719
795
|
}
|
|
720
796
|
try {
|
|
721
797
|
if (process.platform === "win32") {
|
|
722
|
-
await
|
|
798
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
799
|
+
await copyFileFallback(root, params, source);
|
|
800
|
+
});
|
|
723
801
|
return;
|
|
724
802
|
}
|
|
725
|
-
const pinned = await
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
803
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
804
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
805
|
+
let identity;
|
|
806
|
+
try {
|
|
807
|
+
if (getFsSafePythonConfig().mode === "off") {
|
|
808
|
+
await copyFileFallback(root, params, source);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
identity = await runPinnedCopyHelper({
|
|
812
|
+
rootPath: pinned.rootReal,
|
|
813
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
814
|
+
basename: pinned.basename,
|
|
815
|
+
mkdir: params.mkdir !== false,
|
|
816
|
+
mode: pinned.mode,
|
|
817
|
+
overwrite: true,
|
|
818
|
+
maxBytes: params.maxBytes,
|
|
819
|
+
sourcePath: source.realPath,
|
|
820
|
+
sourceIdentity: { dev: source.stat.dev, ino: source.stat.ino },
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
if (canFallbackFromPythonError(error)) {
|
|
825
|
+
await copyFileFallback(root, params, source);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
throw normalizePinnedWriteError(error);
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
await verifyAtomicWriteResult({
|
|
832
|
+
root,
|
|
833
|
+
targetPath: pinned.targetPath,
|
|
834
|
+
expectedIdentity: identity,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
740
841
|
});
|
|
741
|
-
try {
|
|
742
|
-
await verifyAtomicWriteResult({
|
|
743
|
-
root,
|
|
744
|
-
targetPath: pinned.targetPath,
|
|
745
|
-
expectedIdentity: identity,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
catch (err) {
|
|
749
|
-
emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
|
|
750
|
-
throw err;
|
|
751
|
-
}
|
|
752
842
|
}
|
|
753
843
|
finally {
|
|
754
844
|
await source.handle.close().catch(() => { });
|
|
755
845
|
}
|
|
756
846
|
}
|
|
757
|
-
async function
|
|
758
|
-
const { rootReal, rootWithSep, resolved } = await
|
|
847
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode) {
|
|
848
|
+
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
|
|
759
849
|
try {
|
|
760
850
|
await assertNoPathAliasEscape({
|
|
761
851
|
absolutePath: resolved,
|
|
@@ -777,15 +867,15 @@ async function resolvePinnedWriteTargetInSafeRoot(root, relativePath) {
|
|
|
777
867
|
if (!basename || basename === "." || basename === "/") {
|
|
778
868
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
779
869
|
}
|
|
780
|
-
let mode = 0o600;
|
|
870
|
+
let mode = requestedMode ?? 0o600;
|
|
781
871
|
try {
|
|
782
|
-
const opened = await
|
|
872
|
+
const opened = await openFileInRoot(root, {
|
|
783
873
|
relativePath,
|
|
784
|
-
|
|
874
|
+
hardlinks: "reject",
|
|
785
875
|
nonBlockingRead: true,
|
|
786
876
|
});
|
|
787
877
|
try {
|
|
788
|
-
mode = opened.stat.mode & 0o777;
|
|
878
|
+
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
789
879
|
if (!isPathInside(rootWithSep, opened.realPath)) {
|
|
790
880
|
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
791
881
|
}
|
|
@@ -807,33 +897,28 @@ async function resolvePinnedWriteTargetInSafeRoot(root, relativePath) {
|
|
|
807
897
|
mode: mode || 0o600,
|
|
808
898
|
};
|
|
809
899
|
}
|
|
810
|
-
async function
|
|
811
|
-
|
|
900
|
+
async function resolvePinnedPathInRoot(root, params) {
|
|
901
|
+
return await resolvePinnedOperationPathInRoot(root, {
|
|
902
|
+
allowRoot: params.allowRoot,
|
|
812
903
|
relativePath: params.relativePath,
|
|
813
904
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
814
905
|
});
|
|
815
|
-
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
816
|
-
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
817
|
-
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
|
|
818
|
-
}
|
|
819
|
-
if (relativeResolved === "" ||
|
|
820
|
-
relativeResolved === "." ||
|
|
821
|
-
relativeResolved.startsWith("..") ||
|
|
822
|
-
path.isAbsolute(relativeResolved)) {
|
|
823
|
-
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
824
|
-
}
|
|
825
|
-
const relativePosix = relativeResolved.split(path.sep).join(path.posix.sep);
|
|
826
|
-
if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
827
|
-
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
828
|
-
}
|
|
829
|
-
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
830
906
|
}
|
|
831
|
-
async function
|
|
832
|
-
|
|
907
|
+
async function resolvePinnedRemovePathInRoot(root, relativePath) {
|
|
908
|
+
return await resolvePinnedOperationPathInRoot(root, {
|
|
833
909
|
relativePath,
|
|
834
910
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
835
911
|
});
|
|
912
|
+
}
|
|
913
|
+
async function resolvePinnedOperationPathInRoot(root, params) {
|
|
914
|
+
const resolved = await resolvePinnedRootPathInRoot(root, {
|
|
915
|
+
relativePath: params.relativePath,
|
|
916
|
+
policy: params.policy,
|
|
917
|
+
});
|
|
836
918
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
919
|
+
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
920
|
+
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
|
|
921
|
+
}
|
|
837
922
|
if (relativeResolved === "" ||
|
|
838
923
|
relativeResolved === "." ||
|
|
839
924
|
relativeResolved.startsWith("..") ||
|
|
@@ -844,13 +929,9 @@ async function resolvePinnedRemovePathInSafeRoot(root, relativePath) {
|
|
|
844
929
|
if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
845
930
|
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
846
931
|
}
|
|
847
|
-
const parentRelative = path.posix.dirname(relativePosix);
|
|
848
|
-
if (parentRelative === "." || parentRelative === "") {
|
|
849
|
-
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
850
|
-
}
|
|
851
932
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
852
933
|
}
|
|
853
|
-
async function
|
|
934
|
+
async function resolvePinnedRootPathInRoot(root, params) {
|
|
854
935
|
const rootReal = root.rootReal;
|
|
855
936
|
let resolved;
|
|
856
937
|
try {
|
|
@@ -872,54 +953,170 @@ async function resolvePinnedRootPathInSafeRoot(root, params) {
|
|
|
872
953
|
canonicalPath: resolved.canonicalPath,
|
|
873
954
|
};
|
|
874
955
|
}
|
|
875
|
-
function
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
});
|
|
956
|
+
async function removePathFallback(resolved) {
|
|
957
|
+
const guard = await createAsyncDirectoryGuard(path.dirname(resolved.resolved));
|
|
958
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", resolved.resolved);
|
|
959
|
+
await assertAsyncDirectoryGuard(guard);
|
|
960
|
+
await ((await fs.lstat(resolved.resolved)).isDirectory() ? fs.rmdir(resolved.resolved) : fs.rm(resolved.resolved));
|
|
961
|
+
await assertAsyncDirectoryGuard(guard).catch(() => undefined);
|
|
882
962
|
}
|
|
883
|
-
function
|
|
884
|
-
|
|
963
|
+
async function mkdirPathFallback(resolved) {
|
|
964
|
+
await mkdirPathComponentsWithGuards({
|
|
965
|
+
rootReal: resolved.rootReal, targetPath: resolved.resolved,
|
|
966
|
+
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
967
|
+
});
|
|
885
968
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
969
|
+
async function statPathFallback(root, relativePath) {
|
|
970
|
+
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
971
|
+
try {
|
|
972
|
+
return pathStatFromStats(await fs.lstat(resolved.resolved));
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
if (isNotFoundPathError(error)) {
|
|
976
|
+
throw new FsSafeError("not-found", "file not found", {
|
|
977
|
+
cause: error instanceof Error ? error : undefined,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
throw error;
|
|
889
981
|
}
|
|
890
|
-
return new FsSafeError("path-alias", "path is not under root", {
|
|
891
|
-
cause: error instanceof Error ? error : undefined,
|
|
892
|
-
});
|
|
893
982
|
}
|
|
894
|
-
async function
|
|
895
|
-
await
|
|
983
|
+
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
984
|
+
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
985
|
+
try {
|
|
986
|
+
const names = await fs.readdir(resolved.resolved);
|
|
987
|
+
const sortedNames = names.toSorted();
|
|
988
|
+
if (!withFileTypes) {
|
|
989
|
+
return sortedNames;
|
|
990
|
+
}
|
|
991
|
+
const entries = [];
|
|
992
|
+
for (const name of sortedNames) {
|
|
993
|
+
entries.push({
|
|
994
|
+
name,
|
|
995
|
+
...pathStatFromStats(await fs.lstat(path.join(resolved.resolved, name))),
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return entries;
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
if (isNotFoundPathError(error)) {
|
|
1002
|
+
throw new FsSafeError("not-found", "directory not found", {
|
|
1003
|
+
cause: error instanceof Error ? error : undefined,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
896
1008
|
}
|
|
897
|
-
async function
|
|
898
|
-
await
|
|
1009
|
+
async function movePathFallback(root, params) {
|
|
1010
|
+
const source = await resolvePathInRoot(root, params.fromRelative);
|
|
1011
|
+
await resolvePinnedRootPathInRoot(root, {
|
|
1012
|
+
relativePath: params.fromRelative,
|
|
1013
|
+
policy: PATH_ALIAS_POLICIES.strict,
|
|
1014
|
+
});
|
|
1015
|
+
const target = await resolvePathInRoot(root, params.toRelative);
|
|
1016
|
+
await resolvePinnedRootPathInRoot(root, {
|
|
1017
|
+
relativePath: params.toRelative,
|
|
1018
|
+
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
1019
|
+
});
|
|
1020
|
+
try {
|
|
1021
|
+
await assertNoPathAliasEscape({
|
|
1022
|
+
absolutePath: target.resolved,
|
|
1023
|
+
rootPath: target.rootReal,
|
|
1024
|
+
boundaryLabel: "root",
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
throw new FsSafeError("path-alias", "path alias escape blocked", {
|
|
1029
|
+
cause: error instanceof Error ? error : undefined,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
let sourceStat;
|
|
1033
|
+
try {
|
|
1034
|
+
sourceStat = await fs.lstat(source.resolved);
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
if (isNotFoundPathError(error)) {
|
|
1038
|
+
throw new FsSafeError("not-found", "file not found", {
|
|
1039
|
+
cause: error instanceof Error ? error : undefined,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
if (sourceStat.isSymbolicLink()) {
|
|
1045
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
1046
|
+
}
|
|
1047
|
+
if (sourceStat.isFile() && sourceStat.nlink > 1) {
|
|
1048
|
+
throw new FsSafeError("hardlink", "hardlinked path not allowed");
|
|
1049
|
+
}
|
|
1050
|
+
if (!params.overwrite) {
|
|
1051
|
+
try {
|
|
1052
|
+
await fs.lstat(target.resolved);
|
|
1053
|
+
throw new FsSafeError("already-exists", "destination exists");
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
if (error instanceof FsSafeError) {
|
|
1057
|
+
throw error;
|
|
1058
|
+
}
|
|
1059
|
+
if (!isNotFoundPathError(error)) {
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
|
|
1065
|
+
const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
|
|
1066
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("move", target.resolved);
|
|
1067
|
+
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1068
|
+
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1069
|
+
try {
|
|
1070
|
+
await fs.rename(source.resolved, target.resolved);
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
if (isNotFoundPathError(error)) {
|
|
1074
|
+
throw new FsSafeError("not-found", "file not found", {
|
|
1075
|
+
cause: error instanceof Error ? error : undefined,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
if (hasNodeErrorCode(error, "EEXIST")) {
|
|
1079
|
+
throw new FsSafeError("already-exists", "destination exists", {
|
|
1080
|
+
cause: error instanceof Error ? error : undefined,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
throw error;
|
|
1084
|
+
}
|
|
1085
|
+
await assertAsyncDirectoryGuard(targetParentGuard).catch(() => undefined);
|
|
899
1086
|
}
|
|
900
1087
|
async function writeFileFallback(root, params) {
|
|
901
1088
|
if (params.overwrite === false) {
|
|
902
|
-
|
|
1089
|
+
await writeMissingFileFallback(root, params);
|
|
1090
|
+
return;
|
|
903
1091
|
}
|
|
904
|
-
const target = await
|
|
1092
|
+
const target = await openWritableFileInRoot(root, {
|
|
905
1093
|
relativePath: params.relativePath,
|
|
906
1094
|
mkdir: params.mkdir,
|
|
1095
|
+
mode: params.mode,
|
|
907
1096
|
truncateExisting: false,
|
|
908
1097
|
});
|
|
909
1098
|
const destinationPath = target.realPath;
|
|
910
|
-
const
|
|
1099
|
+
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
911
1100
|
await target.handle.close().catch(() => { });
|
|
1101
|
+
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
|
|
912
1102
|
let tempPath = null;
|
|
1103
|
+
let unregisterTempPath = null;
|
|
913
1104
|
try {
|
|
914
1105
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1106
|
+
unregisterTempPath = registerTempPathForExit(tempPath);
|
|
915
1107
|
const writtenStat = await writeTempFileForAtomicReplace({
|
|
916
1108
|
tempPath,
|
|
917
1109
|
data: params.data,
|
|
918
1110
|
encoding: params.encoding,
|
|
919
|
-
mode:
|
|
1111
|
+
mode: mode || 0o600,
|
|
1112
|
+
});
|
|
1113
|
+
const commitTempPath = tempPath;
|
|
1114
|
+
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
1115
|
+
await fs.rename(commitTempPath, destinationPath);
|
|
920
1116
|
});
|
|
921
|
-
await fs.rename(tempPath, destinationPath);
|
|
922
1117
|
tempPath = null;
|
|
1118
|
+
unregisterTempPath();
|
|
1119
|
+
unregisterTempPath = null;
|
|
923
1120
|
try {
|
|
924
1121
|
await verifyAtomicWriteResult({
|
|
925
1122
|
root,
|
|
@@ -936,11 +1133,11 @@ async function writeFileFallback(root, params) {
|
|
|
936
1133
|
if (tempPath) {
|
|
937
1134
|
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
938
1135
|
}
|
|
1136
|
+
unregisterTempPath?.();
|
|
939
1137
|
}
|
|
940
|
-
return true;
|
|
941
1138
|
}
|
|
942
1139
|
async function writeMissingFileFallback(root, params) {
|
|
943
|
-
const { rootReal, resolved } = await
|
|
1140
|
+
const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
944
1141
|
try {
|
|
945
1142
|
await assertNoPathAliasEscape({
|
|
946
1143
|
absolutePath: resolved,
|
|
@@ -954,36 +1151,48 @@ async function writeMissingFileFallback(root, params) {
|
|
|
954
1151
|
if (params.mkdir !== false) {
|
|
955
1152
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
956
1153
|
}
|
|
957
|
-
|
|
1154
|
+
const parentGuard = await createAsyncDirectoryGuard(path.dirname(resolved));
|
|
958
1155
|
let created = false;
|
|
959
1156
|
try {
|
|
960
|
-
handle = await
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1157
|
+
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1158
|
+
const handle = await fs.open(resolved, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
|
|
1159
|
+
created = true;
|
|
1160
|
+
try {
|
|
1161
|
+
if (typeof params.data === "string") {
|
|
1162
|
+
await handle.writeFile(params.data, params.encoding ?? "utf8");
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
await handle.writeFile(params.data);
|
|
1166
|
+
}
|
|
1167
|
+
return { handle, writtenStat: await handle.stat() };
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
await handle.close().catch(() => undefined);
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
}, {
|
|
1174
|
+
onPostGuardFailure: async ({ handle }) => {
|
|
1175
|
+
created = false; // Parent is untrusted now; skip outer path cleanup by name.
|
|
1176
|
+
await handle.close().catch(() => undefined);
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
969
1179
|
await handle.close();
|
|
970
|
-
handle = null;
|
|
971
1180
|
await verifyAtomicWriteResult({
|
|
972
1181
|
root,
|
|
973
1182
|
targetPath: resolved,
|
|
974
1183
|
expectedIdentity: writtenStat,
|
|
975
1184
|
});
|
|
976
1185
|
created = false;
|
|
977
|
-
return true;
|
|
978
1186
|
}
|
|
979
1187
|
catch (err) {
|
|
980
1188
|
if (hasNodeErrorCode(err, "EEXIST")) {
|
|
981
|
-
|
|
1189
|
+
throw new FsSafeError("already-exists", "file already exists", {
|
|
1190
|
+
cause: err instanceof Error ? err : undefined,
|
|
1191
|
+
});
|
|
982
1192
|
}
|
|
983
1193
|
throw err;
|
|
984
1194
|
}
|
|
985
1195
|
finally {
|
|
986
|
-
await handle?.close().catch(() => undefined);
|
|
987
1196
|
if (created) {
|
|
988
1197
|
await fs.rm(resolved, { force: true }).catch(() => undefined);
|
|
989
1198
|
}
|
|
@@ -995,20 +1204,24 @@ async function copyFileFallback(root, params, source) {
|
|
|
995
1204
|
let targetClosedByUs = false;
|
|
996
1205
|
let tempHandle = null;
|
|
997
1206
|
let tempPath = null;
|
|
1207
|
+
let unregisterTempPath = null;
|
|
998
1208
|
let tempClosedByStream = false;
|
|
999
1209
|
try {
|
|
1000
|
-
target = await
|
|
1210
|
+
target = await openWritableFileInRoot(root, {
|
|
1001
1211
|
relativePath: params.relativePath,
|
|
1002
1212
|
mkdir: params.mkdir,
|
|
1213
|
+
mode: params.mode,
|
|
1003
1214
|
truncateExisting: false,
|
|
1004
1215
|
});
|
|
1005
1216
|
const destinationPath = target.realPath;
|
|
1006
|
-
const
|
|
1217
|
+
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
1007
1218
|
await target.handle.close().catch(() => { });
|
|
1008
1219
|
targetClosedByUs = true;
|
|
1220
|
+
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
|
|
1009
1221
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1010
|
-
|
|
1011
|
-
|
|
1222
|
+
unregisterTempPath = registerTempPathForExit(tempPath);
|
|
1223
|
+
tempHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, mode || 0o600);
|
|
1224
|
+
const sourceStream = createBoundedReadStream(source, params.maxBytes);
|
|
1012
1225
|
const targetStream = tempHandle.createWriteStream();
|
|
1013
1226
|
sourceStream.once("close", () => {
|
|
1014
1227
|
sourceClosedByStream = true;
|
|
@@ -1023,8 +1236,13 @@ async function copyFileFallback(root, params, source) {
|
|
|
1023
1236
|
tempClosedByStream = true;
|
|
1024
1237
|
}
|
|
1025
1238
|
tempHandle = null;
|
|
1026
|
-
|
|
1239
|
+
const commitTempPath = tempPath;
|
|
1240
|
+
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
1241
|
+
await fs.rename(commitTempPath, destinationPath);
|
|
1242
|
+
});
|
|
1027
1243
|
tempPath = null;
|
|
1244
|
+
unregisterTempPath();
|
|
1245
|
+
unregisterTempPath = null;
|
|
1028
1246
|
try {
|
|
1029
1247
|
await verifyAtomicWriteResult({
|
|
1030
1248
|
root,
|
|
@@ -1044,15 +1262,16 @@ async function copyFileFallback(root, params, source) {
|
|
|
1044
1262
|
throw err;
|
|
1045
1263
|
}
|
|
1046
1264
|
finally {
|
|
1047
|
-
if (tempPath) {
|
|
1048
|
-
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
1049
|
-
}
|
|
1050
1265
|
if (!sourceClosedByStream) {
|
|
1051
1266
|
await source.handle.close().catch(() => { });
|
|
1052
1267
|
}
|
|
1053
1268
|
if (tempHandle && !tempClosedByStream) {
|
|
1054
1269
|
await tempHandle.close().catch(() => { });
|
|
1055
1270
|
}
|
|
1271
|
+
if (tempPath) {
|
|
1272
|
+
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
1273
|
+
}
|
|
1274
|
+
unregisterTempPath?.();
|
|
1056
1275
|
if (target && !targetClosedByUs) {
|
|
1057
1276
|
await target.handle.close().catch(() => { });
|
|
1058
1277
|
}
|