@openclaw/fs-safe 0.9.0 → 0.10.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 +78 -0
- package/README.md +17 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-gzip-tail.d.ts +2 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +20 -3
- package/dist/archive-merge.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +3 -2
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +67 -66
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +20 -8
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +2 -1
- package/dist/bounded-read.d.ts +7 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +73 -43
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +23 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +91 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +191 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +123 -0
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-hash.d.ts +6 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +44 -8
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +1 -0
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -6
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +13 -14
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +55 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +26 -31
- package/dist/mutation-authority.d.ts +8 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +24 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +18 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +1 -0
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +38 -48
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +5 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +50 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +51 -26
- package/dist/root-directory-list.d.ts +24 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +201 -0
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +3 -0
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +6 -50
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +330 -210
- package/dist/root-options.d.ts +66 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +56 -1
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-symlink-policy.d.ts +13 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +1 -4
- package/dist/root.d.ts +3 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +4 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +2 -1
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +3 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.js +2 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +6 -26
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +6 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +25 -0
- package/docs/advanced.md +18 -4
- package/docs/archive.md +22 -7
- package/docs/atomic.md +7 -0
- package/docs/contributing.md +7 -0
- package/docs/copy.md +86 -0
- package/docs/durability.md +17 -2
- package/docs/errors.md +1 -1
- package/docs/local-roots.md +8 -1
- package/docs/native.md +14 -1
- package/docs/permissions.md +20 -10
- package/docs/positional-read.md +63 -0
- package/docs/root.md +166 -4
- package/docs/security-model.md +14 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +20 -2
- package/package.json +13 -8
package/dist/root-impl.js
CHANGED
|
@@ -1,8 +1,61 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
import { randomUUID } from "node:crypto";
|
|
2
54
|
import fsSync, { constants as fsConstants } from "node:fs";
|
|
3
55
|
import fs from "node:fs/promises";
|
|
4
56
|
import path from "node:path";
|
|
5
57
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
58
|
+
import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
|
|
6
59
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
7
60
|
import { FsSafeError } from "./errors.js";
|
|
8
61
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
@@ -18,9 +71,11 @@ import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
|
18
71
|
import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
19
72
|
import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
20
73
|
import { readOpenedFileSafely } from "./read-opened-file.js";
|
|
74
|
+
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
21
75
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
22
76
|
import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
|
|
23
77
|
import { resolveRootPath } from "./root-path.js";
|
|
78
|
+
import { listDirectoryForWalk, listDirectoryPath, pathStatFromStats } from "./root-directory-list.js";
|
|
24
79
|
import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
25
80
|
import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, normalizeRemoveGuardError, normalizeRemovePathError, outsideWorkspaceError, } from "./root-errors.js";
|
|
26
81
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
@@ -31,7 +86,12 @@ import { serializePathWrite } from "./write-queue.js";
|
|
|
31
86
|
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
32
87
|
import { inheritWriteTargetMode } from "./root-write-mode.js";
|
|
33
88
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
34
|
-
import { onCopyPublication } from "./copy-publication.js";
|
|
89
|
+
import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
|
|
90
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
91
|
+
import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
|
|
92
|
+
import { mergeReadOptions, readDefaults, } from "./root-options.js";
|
|
93
|
+
import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
|
|
94
|
+
export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
|
|
35
95
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
36
96
|
function logWarn(message) {
|
|
37
97
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
@@ -54,22 +114,6 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
54
114
|
fsConstants.O_CREAT |
|
|
55
115
|
fsConstants.O_EXCL |
|
|
56
116
|
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
57
|
-
export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
58
|
-
function pathStatFromStats(stat) {
|
|
59
|
-
return {
|
|
60
|
-
dev: Number(stat.dev),
|
|
61
|
-
gid: Number(stat.gid),
|
|
62
|
-
ino: Number(stat.ino),
|
|
63
|
-
isDirectory: stat.isDirectory(),
|
|
64
|
-
isFile: stat.isFile(),
|
|
65
|
-
isSymbolicLink: stat.isSymbolicLink(),
|
|
66
|
-
mode: stat.mode,
|
|
67
|
-
mtimeMs: stat.mtimeMs,
|
|
68
|
-
nlink: stat.nlink,
|
|
69
|
-
size: stat.size,
|
|
70
|
-
uid: stat.uid,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
117
|
function openResult(params) {
|
|
74
118
|
return {
|
|
75
119
|
handle: params.handle,
|
|
@@ -215,12 +259,14 @@ export class RootHandle {
|
|
|
215
259
|
mutationOptions(options) {
|
|
216
260
|
return {
|
|
217
261
|
...options,
|
|
262
|
+
assertBeforeMutation: composeMutationAssertions(this.defaults.assertBeforeMutation, options.assertBeforeMutation),
|
|
263
|
+
mutationSymlinks: options.mutationSymlinks ?? this.defaults.mutationSymlinks,
|
|
218
264
|
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
219
265
|
};
|
|
220
266
|
}
|
|
221
267
|
async resolve(relativePath) {
|
|
222
268
|
assertValidRootDestinationPath(relativePath);
|
|
223
|
-
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
|
|
269
|
+
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true, resolveCanonical: true })).resolved;
|
|
224
270
|
}
|
|
225
271
|
async open(relativePath, options = {}) {
|
|
226
272
|
return await openFileInRoot(this.context, {
|
|
@@ -266,7 +312,7 @@ export class RootHandle {
|
|
|
266
312
|
...this.mutationOptions(options),
|
|
267
313
|
append: writeMode === "append",
|
|
268
314
|
truncateExisting: writeMode === "replace",
|
|
269
|
-
});
|
|
315
|
+
}).catch(rethrowMutationAuthorityError);
|
|
270
316
|
}
|
|
271
317
|
async append(relativePath, data, options = {}) {
|
|
272
318
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -277,28 +323,28 @@ export class RootHandle {
|
|
|
277
323
|
mode: this.defaults.mode,
|
|
278
324
|
...this.mutationOptions(options),
|
|
279
325
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
280
|
-
});
|
|
326
|
+
}).catch(rethrowMutationAuthorityError);
|
|
281
327
|
}
|
|
282
328
|
async remove(relativePath, options = {}) {
|
|
283
329
|
assertValidRootRelativePath(relativePath);
|
|
284
330
|
await removePathInRoot(this.context, {
|
|
285
331
|
relativePath,
|
|
286
332
|
...this.mutationOptions(options),
|
|
287
|
-
});
|
|
333
|
+
}).catch(rethrowMutationAuthorityError);
|
|
288
334
|
}
|
|
289
335
|
async mkdir(relativePath, options = {}) {
|
|
290
336
|
assertValidRootDestinationPath(relativePath);
|
|
291
337
|
await mkdirPathInRoot(this.context, {
|
|
292
338
|
relativePath,
|
|
293
339
|
...this.mutationOptions(options),
|
|
294
|
-
});
|
|
340
|
+
}).catch(rethrowMutationAuthorityError);
|
|
295
341
|
}
|
|
296
342
|
async ensureRoot(options = {}) {
|
|
297
343
|
await mkdirPathInRoot(this.context, {
|
|
298
344
|
relativePath: "",
|
|
299
345
|
allowRoot: true,
|
|
300
346
|
...this.mutationOptions(options),
|
|
301
|
-
});
|
|
347
|
+
}).catch(rethrowMutationAuthorityError);
|
|
302
348
|
}
|
|
303
349
|
async write(relativePath, data, options = {}) {
|
|
304
350
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -310,7 +356,7 @@ export class RootHandle {
|
|
|
310
356
|
renameIdentity: this.defaults.renameIdentity,
|
|
311
357
|
...this.mutationOptions(options),
|
|
312
358
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
313
|
-
});
|
|
359
|
+
}).catch(rethrowMutationAuthorityError);
|
|
314
360
|
}
|
|
315
361
|
async create(relativePath, data, options = {}) {
|
|
316
362
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -322,7 +368,7 @@ export class RootHandle {
|
|
|
322
368
|
...this.mutationOptions(options),
|
|
323
369
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
324
370
|
overwrite: false,
|
|
325
|
-
});
|
|
371
|
+
}).catch(rethrowMutationAuthorityError);
|
|
326
372
|
}
|
|
327
373
|
async writeJson(relativePath, data, options = {}) {
|
|
328
374
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
@@ -334,19 +380,20 @@ export class RootHandle {
|
|
|
334
380
|
const json = stringifyJsonDocument(data, replacer, space);
|
|
335
381
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
336
382
|
}
|
|
337
|
-
async copyIn(relativePath,
|
|
383
|
+
async copyIn(relativePath, source, options = {}) {
|
|
384
|
+
options.signal?.throwIfAborted();
|
|
338
385
|
assertValidRootDestinationPath(relativePath);
|
|
339
386
|
const { maxBytes, ...copyOptions } = this.mutationOptions(options);
|
|
340
387
|
await copyFileInRoot(this.context, {
|
|
341
|
-
|
|
388
|
+
source: typeof source === "string" ? source : { root: source.root, relativePath: source.relativePath },
|
|
342
389
|
relativePath,
|
|
343
390
|
maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
|
|
344
391
|
mkdir: this.defaults.mkdir,
|
|
345
|
-
mode: this.defaults.mode,
|
|
346
392
|
...copyOptions,
|
|
393
|
+
mode: options.mode ?? this.defaults.mode,
|
|
347
394
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
348
395
|
verifyPublished: options[onCopyPublication],
|
|
349
|
-
});
|
|
396
|
+
}).catch(rethrowMutationAuthorityError);
|
|
350
397
|
}
|
|
351
398
|
async exists(relativePath) {
|
|
352
399
|
try {
|
|
@@ -374,7 +421,7 @@ export class RootHandle {
|
|
|
374
421
|
assertValidRootRelativePath(fromRelative);
|
|
375
422
|
assertValidRootDestinationPath(toRelative);
|
|
376
423
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
377
|
-
const { denyMutations } = this.mutationOptions(options);
|
|
424
|
+
const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
|
|
378
425
|
await assertMoveMutationAllowed(this.context, {
|
|
379
426
|
fromRelative,
|
|
380
427
|
toRelative,
|
|
@@ -383,31 +430,25 @@ export class RootHandle {
|
|
|
383
430
|
await movePathFallback(this.context, {
|
|
384
431
|
fromRelative,
|
|
385
432
|
denyMutations,
|
|
433
|
+
assertBeforeMutation,
|
|
434
|
+
mutationSymlinks,
|
|
386
435
|
overwrite: options.overwrite ?? false,
|
|
387
436
|
toRelative,
|
|
388
|
-
});
|
|
437
|
+
}).catch(rethrowMutationAuthorityError);
|
|
389
438
|
}
|
|
390
439
|
walk(relativePath, options) {
|
|
391
440
|
assertValidRootRelativePath(relativePath);
|
|
392
|
-
return walkRoot(
|
|
441
|
+
return walkRoot({
|
|
442
|
+
rootReal: this.rootReal,
|
|
443
|
+
stat: relative => this.stat(relative),
|
|
444
|
+
list: async (relative, listingOptions) => {
|
|
445
|
+
validatePinnedOperationPayload({ relativePath: relative });
|
|
446
|
+
const resolved = await resolvePinnedPathInRoot(this.context, { relativePath: relative, allowRoot: true });
|
|
447
|
+
return await listDirectoryForWalk(this.context, resolved.resolved, listingOptions);
|
|
448
|
+
},
|
|
449
|
+
}, relativePath, options);
|
|
393
450
|
}
|
|
394
451
|
}
|
|
395
|
-
function readDefaults(defaults) {
|
|
396
|
-
return {
|
|
397
|
-
hardlinks: defaults.hardlinks,
|
|
398
|
-
maxBytes: normalizeMaxBytes(defaults.maxBytes, { defaultValue: DEFAULT_ROOT_MAX_BYTES }),
|
|
399
|
-
symlinks: defaults.symlinks,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
function mergeReadOptions(defaults, options) {
|
|
403
|
-
const merged = readDefaults(defaults);
|
|
404
|
-
if (options.hardlinks !== undefined)
|
|
405
|
-
merged.hardlinks = options.hardlinks;
|
|
406
|
-
merged.maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: merged.maxBytes });
|
|
407
|
-
if (options.symlinks !== undefined)
|
|
408
|
-
merged.symlinks = options.symlinks;
|
|
409
|
-
return merged;
|
|
410
|
-
}
|
|
411
452
|
export async function root(rootDir, defaults = {}) {
|
|
412
453
|
normalizeMaxBytes(defaults.maxBytes);
|
|
413
454
|
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
@@ -416,7 +457,8 @@ async function openFileInRoot(root, params) {
|
|
|
416
457
|
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
|
|
417
458
|
allowFinalSymlink: true,
|
|
418
459
|
rejectUnsafeDeviceReads: true,
|
|
419
|
-
|
|
460
|
+
...readSymlinkResolution(params.symlinks),
|
|
461
|
+
resolveCanonical: true,
|
|
420
462
|
});
|
|
421
463
|
const { opened } = await openVerifiedLocalFile(resolved, {
|
|
422
464
|
symlinks: params.symlinks,
|
|
@@ -472,38 +514,22 @@ function buildAtomicWriteTempPath(targetPath) {
|
|
|
472
514
|
function rootWriteQueueKey(root, relativePath) {
|
|
473
515
|
return `${root.rootReal}\0${relativePath}`;
|
|
474
516
|
}
|
|
475
|
-
async function prepareRootWriteTarget(rootReal, targetPath) {
|
|
517
|
+
async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
|
|
476
518
|
const parentPath = await mkdirPathComponentsWithGuards({
|
|
477
519
|
rootReal,
|
|
478
520
|
targetPath: path.dirname(targetPath),
|
|
521
|
+
assertBeforeMutation,
|
|
479
522
|
});
|
|
480
523
|
// Continue through the guarded walk's real parent instead of re-entering
|
|
481
524
|
// the original path through a symlinked component.
|
|
482
525
|
return path.join(parentPath, path.basename(targetPath));
|
|
483
526
|
}
|
|
484
|
-
async function writeTempFileForAtomicReplace(params) {
|
|
485
|
-
const tempHandle = await fs.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
|
|
486
|
-
try {
|
|
487
|
-
if (typeof params.data === "string") {
|
|
488
|
-
await tempHandle.writeFile(params.data, params.encoding ?? "utf8");
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
await tempHandle.writeFile(params.data);
|
|
492
|
-
}
|
|
493
|
-
return {
|
|
494
|
-
handle: tempHandle,
|
|
495
|
-
identity: fsSync.fstatSync(tempHandle.fd, { bigint: true }),
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
await tempHandle.close().catch(() => { });
|
|
500
|
-
throw error;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
527
|
async function resolveGuardedWritePathInRoot(root, params) {
|
|
504
528
|
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
505
529
|
aliasErrorCode: "path-alias",
|
|
530
|
+
rejectAmbiguousParents: true,
|
|
506
531
|
allowFinalSymlink: params.allowFinalSymlink,
|
|
532
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
507
533
|
});
|
|
508
534
|
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
509
535
|
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
@@ -526,12 +552,14 @@ async function openWritableFileInRoot(root, params) {
|
|
|
526
552
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
527
553
|
relativePath: params.relativePath,
|
|
528
554
|
denyMutations: params.denyMutations,
|
|
555
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
529
556
|
});
|
|
530
557
|
let ioPath = params.mkdir === false
|
|
531
558
|
? resolved
|
|
532
|
-
: await prepareRootWriteTarget(rootReal, resolved);
|
|
559
|
+
: await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
|
|
533
560
|
try {
|
|
534
|
-
|
|
561
|
+
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
562
|
+
const resolvedRealPath = params.mutationSymlinks === undefined ? fsSync.realpathSync.native(ioPath) : ioPath;
|
|
535
563
|
if (!isPathInside(rootWithSep, resolvedRealPath)) {
|
|
536
564
|
throw outsideWorkspaceError();
|
|
537
565
|
}
|
|
@@ -561,6 +589,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
561
589
|
if (!isNotFoundPathError(err)) {
|
|
562
590
|
throw err;
|
|
563
591
|
}
|
|
592
|
+
params.assertBeforeMutation?.();
|
|
564
593
|
handle = await fs.open(ioPath, createFlags, mode);
|
|
565
594
|
createdForWrite = true;
|
|
566
595
|
}
|
|
@@ -619,6 +648,8 @@ async function openWritableFileInRoot(root, params) {
|
|
|
619
648
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
620
649
|
// irreversible side effects if a symlink target changes before validation.
|
|
621
650
|
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
|
|
651
|
+
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
652
|
+
params.assertBeforeMutation?.();
|
|
622
653
|
await handle.truncate(0);
|
|
623
654
|
}
|
|
624
655
|
return {
|
|
@@ -641,42 +672,72 @@ async function openWritableFileInRoot(root, params) {
|
|
|
641
672
|
}
|
|
642
673
|
}
|
|
643
674
|
async function appendFileInRoot(root, params) {
|
|
644
|
-
const
|
|
645
|
-
relativePath: params.relativePath,
|
|
646
|
-
mkdir: params.mkdir,
|
|
647
|
-
mode: params.mode,
|
|
648
|
-
denyMutations: params.denyMutations,
|
|
649
|
-
truncateExisting: false,
|
|
650
|
-
append: true,
|
|
651
|
-
});
|
|
675
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
652
676
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
677
|
+
const target = await openWritableFileInRoot(root, {
|
|
678
|
+
relativePath: params.relativePath,
|
|
679
|
+
mkdir: params.mkdir,
|
|
680
|
+
mode: params.mode,
|
|
681
|
+
denyMutations: params.denyMutations,
|
|
682
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
683
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
684
|
+
truncateExisting: false,
|
|
685
|
+
append: true,
|
|
686
|
+
});
|
|
687
|
+
let dispatched = false;
|
|
688
|
+
// Reverse disposal order closes the handle before path cleanup and retains both failures.
|
|
689
|
+
const cleanup = __addDisposableResource(env_1, {
|
|
690
|
+
async [Symbol.asyncDispose]() {
|
|
691
|
+
if (!dispatched && target.createdForWrite) {
|
|
692
|
+
await removePathIfIdentityUnchanged(target.realPath, target.stat);
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
}, true);
|
|
696
|
+
const handle = __addDisposableResource(env_1, target.handle, true);
|
|
697
|
+
try {
|
|
698
|
+
let prefix = "";
|
|
699
|
+
if (params.prependNewlineIfNeeded === true &&
|
|
700
|
+
!target.createdForWrite &&
|
|
701
|
+
target.stat.size > 0 &&
|
|
702
|
+
((typeof params.data === "string" && !params.data.startsWith("\n")) ||
|
|
703
|
+
(Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 0x0a))) {
|
|
704
|
+
const newline = Buffer.from("\n", typeof params.data === "string" ? params.encoding : "utf8");
|
|
705
|
+
const tail = Buffer.alloc(newline.length);
|
|
706
|
+
const { bytesRead } = await target.handle.read(tail, 0, tail.length, Math.max(0, target.stat.size - tail.length));
|
|
707
|
+
if (bytesRead > 0 && (bytesRead !== newline.length || !tail.equals(newline))) {
|
|
708
|
+
prefix = "\n";
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const payload = typeof params.data === "string" ? `${prefix}${params.data}`
|
|
712
|
+
: prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
713
|
+
await writeAllToFile(target.handle, payload, {
|
|
714
|
+
encoding: params.encoding,
|
|
715
|
+
assertBeforeMutation: () => {
|
|
716
|
+
assertFinalSymlinkRejected(target.realPath, params.mutationSymlinks !== undefined);
|
|
717
|
+
params.assertBeforeMutation?.();
|
|
718
|
+
dispatched = true;
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
// A successful empty append still creates the file, as Node's appendFile does.
|
|
722
|
+
dispatched = true;
|
|
723
|
+
if (params.durable !== false)
|
|
724
|
+
await target.handle.sync();
|
|
725
|
+
if (params.durable !== false && target.createdForWrite) {
|
|
726
|
+
await syncDirectoryBestEffort(path.dirname(target.realPath));
|
|
663
727
|
}
|
|
664
728
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
670
|
-
await target.handle.appendFile(payload);
|
|
671
|
-
}
|
|
672
|
-
if (params.durable !== false)
|
|
673
|
-
await target.handle.sync();
|
|
674
|
-
if (params.durable !== false && target.createdForWrite) {
|
|
675
|
-
await syncDirectoryBestEffort(path.dirname(target.realPath));
|
|
729
|
+
catch (error) {
|
|
730
|
+
rethrowMutationAuthorityError(error);
|
|
676
731
|
}
|
|
677
732
|
}
|
|
733
|
+
catch (e_1) {
|
|
734
|
+
env_1.error = e_1;
|
|
735
|
+
env_1.hasError = true;
|
|
736
|
+
}
|
|
678
737
|
finally {
|
|
679
|
-
|
|
738
|
+
const result_1 = __disposeResources(env_1);
|
|
739
|
+
if (result_1)
|
|
740
|
+
await result_1;
|
|
680
741
|
}
|
|
681
742
|
}
|
|
682
743
|
async function removePathInRoot(root, params) {
|
|
@@ -684,10 +745,11 @@ async function removePathInRoot(root, params) {
|
|
|
684
745
|
const resolved = await resolvePinnedPathInRoot(root, {
|
|
685
746
|
relativePath: params.relativePath,
|
|
686
747
|
denyMutations: params.denyMutations,
|
|
748
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
687
749
|
remove: true,
|
|
688
750
|
});
|
|
689
751
|
try {
|
|
690
|
-
await removePathFallback(resolved);
|
|
752
|
+
await removePathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
|
|
691
753
|
}
|
|
692
754
|
catch (error) {
|
|
693
755
|
throw normalizePinnedPathError(error);
|
|
@@ -697,7 +759,7 @@ async function mkdirPathInRoot(root, params) {
|
|
|
697
759
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
698
760
|
const resolved = await resolvePinnedPathInRoot(root, params);
|
|
699
761
|
try {
|
|
700
|
-
await mkdirPathFallback(resolved);
|
|
762
|
+
await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
|
|
701
763
|
}
|
|
702
764
|
catch (error) {
|
|
703
765
|
throw normalizePinnedPathError(error);
|
|
@@ -710,7 +772,7 @@ async function writeFileInRoot(root, params) {
|
|
|
710
772
|
await writeFileFallback(root, params);
|
|
711
773
|
return;
|
|
712
774
|
}
|
|
713
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite);
|
|
775
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite, params.mutationSymlinks);
|
|
714
776
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
715
777
|
await commitPinnedWriteInRoot(root, pinned, params);
|
|
716
778
|
});
|
|
@@ -729,8 +791,10 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
729
791
|
mode: params.mode ?? pinned.mode,
|
|
730
792
|
sync: params.durable !== false,
|
|
731
793
|
overwrite: params.overwrite,
|
|
794
|
+
rejectFinalSymlink: params.mutationSymlinks !== undefined,
|
|
732
795
|
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
733
796
|
rootIdentity: root.rootIdentity,
|
|
797
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
734
798
|
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
735
799
|
verifyingPublication = true;
|
|
736
800
|
try {
|
|
@@ -765,46 +829,75 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
765
829
|
}
|
|
766
830
|
}
|
|
767
831
|
async function copyFileInRoot(root, params) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
832
|
+
params.signal?.throwIfAborted();
|
|
833
|
+
const clone = resolveFileCopyCloneMode(params.clone);
|
|
834
|
+
let source;
|
|
835
|
+
let sourceIdentity;
|
|
836
|
+
if (typeof params.source === "string") {
|
|
837
|
+
assertNoNulPathInput(params.source, "source path contains a NUL byte");
|
|
838
|
+
({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
|
|
839
|
+
hardlinks: params.sourceHardlinks,
|
|
840
|
+
}));
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
source = await params.source.root.open(params.source.relativePath, params.sourceHardlinks === undefined ? undefined : { hardlinks: params.sourceHardlinks });
|
|
844
|
+
try {
|
|
845
|
+
sourceIdentity = await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }));
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
await source.handle.close().catch(() => undefined);
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
773
852
|
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
774
853
|
await source.handle.close().catch(() => { });
|
|
775
854
|
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
776
855
|
}
|
|
777
856
|
try {
|
|
778
857
|
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
779
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
858
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode ?? (params.preserveSourceMode ? Number(sourceIdentity.mode & 4095n) : undefined), params.denyMutations, params.overwrite !== false, params.mutationSymlinks);
|
|
780
859
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
781
860
|
await assertCopySourceCurrent(source, sourceIdentity);
|
|
782
|
-
|
|
861
|
+
const verifySource = async () => {
|
|
862
|
+
params.signal?.throwIfAborted();
|
|
863
|
+
if (typeof params.source !== "string") {
|
|
864
|
+
await params.source.root.stat(".");
|
|
865
|
+
}
|
|
866
|
+
await assertCopySourceCurrent(source, sourceIdentity);
|
|
867
|
+
};
|
|
868
|
+
const observer = createCopyPublicationObserver(pinned.targetPath, params.onDestinationPublished);
|
|
783
869
|
try {
|
|
784
|
-
|
|
870
|
+
await runPinnedWriteHelper({
|
|
785
871
|
rootPath: pinned.rootReal,
|
|
786
872
|
relativeParentPath: pinned.relativeParentPath,
|
|
787
873
|
basename: pinned.basename,
|
|
788
874
|
mkdir: params.mkdir !== false,
|
|
789
875
|
mode: pinned.mode,
|
|
790
|
-
overwrite:
|
|
876
|
+
overwrite: params.overwrite !== false,
|
|
877
|
+
rejectFinalSymlink: params.mutationSymlinks !== undefined,
|
|
791
878
|
maxBytes: params.maxBytes,
|
|
792
879
|
sync: params.durable !== false,
|
|
880
|
+
assertBeforeMutation: () => {
|
|
881
|
+
if (params.signal?.aborted)
|
|
882
|
+
throw new MutationAuthorityError(params.signal.reason);
|
|
883
|
+
params.assertBeforeMutation?.();
|
|
884
|
+
},
|
|
793
885
|
verifyPublished: params.verifyPublished,
|
|
794
|
-
|
|
886
|
+
onPublished: observer.onPublished,
|
|
887
|
+
input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
|
|
795
888
|
rootIdentity: root.rootIdentity,
|
|
796
889
|
});
|
|
797
890
|
}
|
|
798
891
|
catch (error) {
|
|
892
|
+
observer.rethrowObserverFailure(error);
|
|
893
|
+
if (params.signal?.aborted && error === params.signal.reason)
|
|
894
|
+
throw error;
|
|
895
|
+
if (isAlreadyExistsError(error)) {
|
|
896
|
+
throw new FsSafeError("already-exists", "copy destination already exists", { cause: error });
|
|
897
|
+
}
|
|
799
898
|
throw normalizePinnedWriteError(error);
|
|
800
899
|
}
|
|
801
|
-
|
|
802
|
-
await assertCopySourcePathCurrent(source, sourceIdentity);
|
|
803
|
-
}
|
|
804
|
-
catch (error) {
|
|
805
|
-
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
806
|
-
throw error;
|
|
807
|
-
}
|
|
900
|
+
await verifySource();
|
|
808
901
|
});
|
|
809
902
|
});
|
|
810
903
|
}
|
|
@@ -812,33 +905,20 @@ async function copyFileInRoot(root, params) {
|
|
|
812
905
|
await source.handle.close().catch(() => { });
|
|
813
906
|
}
|
|
814
907
|
}
|
|
815
|
-
async function assertCopySourceCurrent(source, identity) {
|
|
816
|
-
await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }), identity);
|
|
817
|
-
await assertCopySourcePathCurrent(source, identity);
|
|
818
|
-
}
|
|
819
|
-
async function assertCopySourcePathCurrent(source, identity) {
|
|
820
|
-
await inspectFileIdentity(async () => {
|
|
821
|
-
const current = fsSync.lstatSync(source.realPath, { bigint: true });
|
|
822
|
-
if (current.isSymbolicLink() || !current.isFile()) {
|
|
823
|
-
throw new FsSafeError("path-mismatch", "copy source path changed");
|
|
824
|
-
}
|
|
825
|
-
return current;
|
|
826
|
-
}, identity);
|
|
827
|
-
}
|
|
828
908
|
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
829
909
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
830
|
-
const current = fsSync.lstatSync(targetPath);
|
|
831
|
-
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity)) {
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
910
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
835
|
-
|
|
911
|
+
const current = fsSync.lstatSync(targetPath);
|
|
912
|
+
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
|
|
913
|
+
return;
|
|
914
|
+
await fs.unlink(targetPath);
|
|
836
915
|
});
|
|
837
916
|
}
|
|
838
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
|
|
917
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
|
|
839
918
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
840
919
|
relativePath,
|
|
841
920
|
denyMutations,
|
|
921
|
+
mutationSymlinks,
|
|
842
922
|
});
|
|
843
923
|
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
844
924
|
// is rejected upstream.
|
|
@@ -884,6 +964,7 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
884
964
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
885
965
|
allowRoot: params.allowRoot,
|
|
886
966
|
denyMutations: params.denyMutations,
|
|
967
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
887
968
|
protectDenyMutationAncestors: params.remove === true,
|
|
888
969
|
relativePath: params.relativePath,
|
|
889
970
|
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
@@ -893,6 +974,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
893
974
|
const resolved = await resolvePinnedRootPathInRoot(root, {
|
|
894
975
|
relativePath: params.relativePath,
|
|
895
976
|
policy: params.policy,
|
|
977
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
896
978
|
});
|
|
897
979
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
898
980
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
@@ -929,9 +1011,12 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
929
1011
|
rootCanonicalPath: rootReal,
|
|
930
1012
|
boundaryLabel: "root",
|
|
931
1013
|
policy: params.policy,
|
|
1014
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
932
1015
|
});
|
|
933
1016
|
}
|
|
934
1017
|
catch (err) {
|
|
1018
|
+
if (err instanceof FsSafeError && err.code === "symlink")
|
|
1019
|
+
throw err;
|
|
935
1020
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
936
1021
|
}
|
|
937
1022
|
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
@@ -952,19 +1037,23 @@ async function prepareRemoveGuard(targetPath) {
|
|
|
952
1037
|
throw normalizeRemoveGuardError(error);
|
|
953
1038
|
}
|
|
954
1039
|
}
|
|
955
|
-
async function removePathFallback(resolved) {
|
|
1040
|
+
async function removePathFallback(resolved, assertBeforeMutation, rejectFinalSymlink = false) {
|
|
956
1041
|
const guard = await prepareRemoveGuard(resolved.resolved);
|
|
957
1042
|
try {
|
|
958
|
-
|
|
1043
|
+
const isDirectory = fsSync.lstatSync(resolved.resolved).isDirectory();
|
|
1044
|
+
assertFinalSymlinkRejected(resolved.resolved, rejectFinalSymlink);
|
|
1045
|
+
assertBeforeMutation?.();
|
|
1046
|
+
await (isDirectory ? fs.rmdir(resolved.resolved) : fs.unlink(resolved.resolved));
|
|
959
1047
|
}
|
|
960
1048
|
catch (error) {
|
|
961
1049
|
throw normalizeRemovePathError(error);
|
|
962
1050
|
}
|
|
963
|
-
await assertAsyncDirectoryGuard(guard).catch(
|
|
1051
|
+
await assertAsyncDirectoryGuard(guard).catch(error => { throw normalizeRemoveGuardError(error); });
|
|
964
1052
|
}
|
|
965
|
-
async function mkdirPathFallback(resolved) {
|
|
1053
|
+
async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
|
|
966
1054
|
await mkdirPathComponentsWithGuards({
|
|
967
|
-
rootReal: resolved.rootReal, targetPath: resolved.resolved,
|
|
1055
|
+
rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
|
|
1056
|
+
rejectSymlinks,
|
|
968
1057
|
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
969
1058
|
});
|
|
970
1059
|
}
|
|
@@ -984,31 +1073,7 @@ async function statPathFallback(root, relativePath) {
|
|
|
984
1073
|
}
|
|
985
1074
|
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
986
1075
|
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
987
|
-
|
|
988
|
-
const names = await fs.readdir(resolved.resolved);
|
|
989
|
-
const sortedNames = names.toSorted();
|
|
990
|
-
if (!withFileTypes) {
|
|
991
|
-
await assertRootIdentityCurrent(root);
|
|
992
|
-
return sortedNames;
|
|
993
|
-
}
|
|
994
|
-
const entries = [];
|
|
995
|
-
for (const name of sortedNames) {
|
|
996
|
-
entries.push({
|
|
997
|
-
name,
|
|
998
|
-
...pathStatFromStats(fsSync.lstatSync(path.join(resolved.resolved, name))),
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
await assertRootIdentityCurrent(root);
|
|
1002
|
-
return entries;
|
|
1003
|
-
}
|
|
1004
|
-
catch (error) {
|
|
1005
|
-
if (isNotFoundPathError(error)) {
|
|
1006
|
-
throw new FsSafeError("not-found", "directory not found", {
|
|
1007
|
-
cause: error instanceof Error ? error : undefined,
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
throw error;
|
|
1011
|
-
}
|
|
1076
|
+
return await listDirectoryPath(root, resolved.resolved, withFileTypes);
|
|
1012
1077
|
}
|
|
1013
1078
|
async function assertMoveMutationAllowed(root, params) {
|
|
1014
1079
|
// Keep this preflight separate from the pinned resolutions in movePathFallback:
|
|
@@ -1028,15 +1093,18 @@ async function movePathFallback(root, params) {
|
|
|
1028
1093
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
1029
1094
|
aliasErrorCode: "path-alias",
|
|
1030
1095
|
allowFinalSymlink: true,
|
|
1096
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1031
1097
|
});
|
|
1032
1098
|
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1033
1099
|
await resolvePinnedRootPathInRoot(root, {
|
|
1034
1100
|
relativePath: params.fromRelative,
|
|
1035
1101
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1102
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1036
1103
|
});
|
|
1037
1104
|
const target = await resolveGuardedWritePathInRoot(root, {
|
|
1038
1105
|
relativePath: params.toRelative,
|
|
1039
1106
|
denyMutations: params.denyMutations,
|
|
1107
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1040
1108
|
allowFinalSymlink: true,
|
|
1041
1109
|
protectDeniedAncestors: true,
|
|
1042
1110
|
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
@@ -1093,6 +1161,9 @@ async function movePathFallback(root, params) {
|
|
|
1093
1161
|
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1094
1162
|
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1095
1163
|
try {
|
|
1164
|
+
assertFinalSymlinkRejected(source.resolved, params.mutationSymlinks !== undefined);
|
|
1165
|
+
assertFinalSymlinkRejected(target.resolved, params.mutationSymlinks !== undefined);
|
|
1166
|
+
params.assertBeforeMutation?.();
|
|
1096
1167
|
await fs.rename(source.resolved, target.resolved);
|
|
1097
1168
|
}
|
|
1098
1169
|
catch (error) {
|
|
@@ -1106,7 +1177,13 @@ async function movePathFallback(root, params) {
|
|
|
1106
1177
|
}
|
|
1107
1178
|
throw error;
|
|
1108
1179
|
}
|
|
1109
|
-
|
|
1180
|
+
try {
|
|
1181
|
+
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1182
|
+
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
throw normalizePinnedPathError(error);
|
|
1186
|
+
}
|
|
1110
1187
|
}
|
|
1111
1188
|
async function writeFileFallback(root, params) {
|
|
1112
1189
|
if (params.overwrite === false) {
|
|
@@ -1119,47 +1196,76 @@ async function writeFileFallback(root, params) {
|
|
|
1119
1196
|
// Private, writable placeholder: Windows cannot rename over a read-only file.
|
|
1120
1197
|
mode: 0o600,
|
|
1121
1198
|
denyMutations: params.denyMutations,
|
|
1199
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
1200
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1122
1201
|
truncateExisting: false,
|
|
1123
1202
|
});
|
|
1124
1203
|
const destinationPath = target.realPath;
|
|
1125
1204
|
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
1126
|
-
await
|
|
1127
|
-
|
|
1205
|
+
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
|
|
1206
|
+
await target.handle.close().catch(() => undefined);
|
|
1207
|
+
throw error;
|
|
1208
|
+
});
|
|
1128
1209
|
let tempPath = null;
|
|
1129
1210
|
let unregisterTempPath = null;
|
|
1130
1211
|
let writtenHandle;
|
|
1212
|
+
let writtenIdentity;
|
|
1213
|
+
let placeholderIdentity;
|
|
1214
|
+
let published = false;
|
|
1131
1215
|
try {
|
|
1216
|
+
if (target.createdForWrite)
|
|
1217
|
+
placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
|
|
1132
1218
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1219
|
+
params.assertBeforeMutation?.();
|
|
1220
|
+
writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
|
|
1221
|
+
writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
|
|
1222
|
+
unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
|
|
1223
|
+
await writeAllToFile(writtenHandle, params.data, {
|
|
1224
|
+
encoding: params.encoding, assertBeforeMutation: params.assertBeforeMutation,
|
|
1139
1225
|
});
|
|
1140
|
-
|
|
1141
|
-
|
|
1226
|
+
if (params.durable !== false)
|
|
1227
|
+
await writtenHandle.sync();
|
|
1142
1228
|
const commitTempPath = tempPath;
|
|
1229
|
+
const commitHandle = writtenHandle;
|
|
1230
|
+
const commitIdentity = writtenIdentity;
|
|
1143
1231
|
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
1232
|
+
await verifyAtomicWriteResult({
|
|
1233
|
+
root, targetPath: commitTempPath, fd: commitHandle.fd,
|
|
1234
|
+
expectedIdentity: commitIdentity, parentGuard: destinationGuard,
|
|
1235
|
+
});
|
|
1236
|
+
if (target.createdForWrite) {
|
|
1237
|
+
await cleanupPinnedFilePath({
|
|
1238
|
+
pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
// Windows cannot replace a destination while its old handle remains open.
|
|
1242
|
+
await target.handle.close();
|
|
1243
|
+
assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
|
|
1244
|
+
params.assertBeforeMutation?.();
|
|
1144
1245
|
await fs.rename(commitTempPath, destinationPath);
|
|
1145
1246
|
tempPath = null;
|
|
1247
|
+
published = true;
|
|
1146
1248
|
});
|
|
1147
1249
|
unregisterTempPath();
|
|
1148
1250
|
unregisterTempPath = null;
|
|
1149
1251
|
// Final mode via the retained handle after publication, as the native writer does.
|
|
1150
1252
|
try {
|
|
1151
|
-
await
|
|
1253
|
+
await writtenHandle.chmod(mode);
|
|
1152
1254
|
}
|
|
1153
1255
|
catch (error) {
|
|
1154
|
-
await
|
|
1256
|
+
await cleanupPinnedFilePath({
|
|
1257
|
+
pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
|
|
1258
|
+
});
|
|
1155
1259
|
throw error;
|
|
1156
1260
|
}
|
|
1261
|
+
if (params.durable !== false)
|
|
1262
|
+
await writtenHandle.sync();
|
|
1157
1263
|
try {
|
|
1158
1264
|
await verifyAtomicWriteResult({
|
|
1159
1265
|
root,
|
|
1160
1266
|
targetPath: destinationPath,
|
|
1161
|
-
expectedIdentity:
|
|
1162
|
-
fd:
|
|
1267
|
+
expectedIdentity: writtenIdentity,
|
|
1268
|
+
fd: writtenHandle.fd,
|
|
1163
1269
|
parentGuard: destinationGuard,
|
|
1164
1270
|
});
|
|
1165
1271
|
}
|
|
@@ -1167,12 +1273,22 @@ async function writeFileFallback(root, params) {
|
|
|
1167
1273
|
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
1168
1274
|
throw err;
|
|
1169
1275
|
}
|
|
1276
|
+
if (params.durable !== false)
|
|
1277
|
+
await syncDirectoryBestEffort(path.dirname(destinationPath));
|
|
1170
1278
|
}
|
|
1171
1279
|
finally {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1280
|
+
if (!published && target.createdForWrite) {
|
|
1281
|
+
await cleanupPinnedFilePath({
|
|
1282
|
+
pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
await target.handle.close().catch(() => undefined);
|
|
1286
|
+
if (tempPath && writtenHandle) {
|
|
1287
|
+
await cleanupPinnedFilePath({
|
|
1288
|
+
pathname: tempPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
|
|
1289
|
+
});
|
|
1175
1290
|
}
|
|
1291
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1176
1292
|
unregisterTempPath?.();
|
|
1177
1293
|
}
|
|
1178
1294
|
}
|
|
@@ -1180,38 +1296,38 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1180
1296
|
const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
1181
1297
|
relativePath: params.relativePath,
|
|
1182
1298
|
denyMutations: params.denyMutations,
|
|
1299
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1183
1300
|
});
|
|
1184
1301
|
const targetPath = params.mkdir === false
|
|
1185
1302
|
? resolved
|
|
1186
|
-
: await prepareRootWriteTarget(rootReal, resolved);
|
|
1187
|
-
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
1303
|
+
: await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
|
|
1304
|
+
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
|
|
1188
1305
|
let created = false;
|
|
1189
1306
|
let createdIdentity;
|
|
1190
1307
|
let writtenHandle;
|
|
1191
1308
|
let verifyingPublication = false;
|
|
1192
1309
|
try {
|
|
1193
1310
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1311
|
+
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1312
|
+
params.assertBeforeMutation?.();
|
|
1194
1313
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
|
|
1314
|
+
writtenHandle = handle;
|
|
1195
1315
|
created = true;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
await handle.close().catch(() => undefined);
|
|
1209
|
-
throw error;
|
|
1210
|
-
}
|
|
1316
|
+
const writtenStat = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
1317
|
+
createdIdentity = writtenStat;
|
|
1318
|
+
await writeAllToFile(handle, params.data, {
|
|
1319
|
+
encoding: params.encoding,
|
|
1320
|
+
assertBeforeMutation: () => {
|
|
1321
|
+
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1322
|
+
params.assertBeforeMutation?.();
|
|
1323
|
+
},
|
|
1324
|
+
});
|
|
1325
|
+
if (params.durable !== false)
|
|
1326
|
+
await handle.sync();
|
|
1327
|
+
return { handle, writtenStat };
|
|
1211
1328
|
}, {
|
|
1212
|
-
onPostGuardFailure:
|
|
1329
|
+
onPostGuardFailure: () => {
|
|
1213
1330
|
created = false; // Parent is untrusted now; skip outer path cleanup by name.
|
|
1214
|
-
await handle.close().catch(() => undefined);
|
|
1215
1331
|
},
|
|
1216
1332
|
});
|
|
1217
1333
|
writtenHandle = handle;
|
|
@@ -1224,6 +1340,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1224
1340
|
fd: handle.fd,
|
|
1225
1341
|
parentGuard,
|
|
1226
1342
|
});
|
|
1343
|
+
if (params.durable !== false)
|
|
1344
|
+
await syncDirectoryBestEffort(path.dirname(targetPath));
|
|
1227
1345
|
}
|
|
1228
1346
|
catch (err) {
|
|
1229
1347
|
if (verifyingPublication)
|
|
@@ -1236,9 +1354,11 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1236
1354
|
throw err;
|
|
1237
1355
|
}
|
|
1238
1356
|
finally {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1357
|
+
if (created && writtenHandle) {
|
|
1358
|
+
await cleanupPinnedFilePath({
|
|
1359
|
+
pathname: targetPath, handle: writtenHandle, identity: createdIdentity, parentGuard,
|
|
1360
|
+
});
|
|
1242
1361
|
}
|
|
1362
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1243
1363
|
}
|
|
1244
1364
|
}
|