@openclaw/fs-safe 0.8.6 → 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 +93 -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-durability.d.ts +24 -0
- package/dist/archive-durability.d.ts.map +1 -0
- package/dist/archive-durability.js +180 -0
- 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 +22 -4
- package/dist/archive-input.js +4 -4
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +3 -2
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +42 -9
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +4 -2
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +72 -70
- package/dist/archive-staging.d.ts +1 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +24 -19
- 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/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- 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 +15 -0
- package/dist/copy-publication.d.ts.map +1 -0
- package/dist/copy-publication.js +30 -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/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +4 -4
- 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 +49 -12
- package/dist/file-store-sync-write.d.ts +1 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +9 -6
- package/dist/file-store.d.ts +4 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +10 -1
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +1 -2
- 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 +9 -5
- 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 +9 -13
- package/dist/json-document-store.d.ts +3 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +1 -0
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.js +3 -2
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +31 -27
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -3
- 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 +55 -47
- 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/opened-file-failure.d.ts +1 -1
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +3 -2
- 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/permissions.js +3 -3
- 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/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +9 -3
- 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 +26 -25
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +30 -17
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -5
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +8 -3
- 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 +342 -208
- 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/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secret-read-async.js +5 -5
- package/dist/secure-file.d.ts +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +23 -13
- package/dist/sibling-staged-file.js +7 -7
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +16 -7
- package/dist/sidecar-lock-acquire.d.ts +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +6 -4
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +5 -3
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +16 -4
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -4
- 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 +7 -27
- 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/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -2
- package/docs/advanced.md +18 -4
- package/docs/archive.md +57 -9
- 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/file-store.md +48 -3
- package/docs/json-store.md +10 -0
- 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/reading.md +6 -0
- package/docs/root.md +168 -6
- package/docs/secret-file.md +6 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +2 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +25 -5
- package/package.json +16 -11
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,6 +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";
|
|
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";
|
|
34
95
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
35
96
|
function logWarn(message) {
|
|
36
97
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
@@ -53,22 +114,6 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
53
114
|
fsConstants.O_CREAT |
|
|
54
115
|
fsConstants.O_EXCL |
|
|
55
116
|
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
56
|
-
export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
57
|
-
function pathStatFromStats(stat) {
|
|
58
|
-
return {
|
|
59
|
-
dev: Number(stat.dev),
|
|
60
|
-
gid: Number(stat.gid),
|
|
61
|
-
ino: Number(stat.ino),
|
|
62
|
-
isDirectory: stat.isDirectory(),
|
|
63
|
-
isFile: stat.isFile(),
|
|
64
|
-
isSymbolicLink: stat.isSymbolicLink(),
|
|
65
|
-
mode: stat.mode,
|
|
66
|
-
mtimeMs: stat.mtimeMs,
|
|
67
|
-
nlink: stat.nlink,
|
|
68
|
-
size: stat.size,
|
|
69
|
-
uid: stat.uid,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
117
|
function openResult(params) {
|
|
73
118
|
return {
|
|
74
119
|
handle: params.handle,
|
|
@@ -214,12 +259,14 @@ export class RootHandle {
|
|
|
214
259
|
mutationOptions(options) {
|
|
215
260
|
return {
|
|
216
261
|
...options,
|
|
262
|
+
assertBeforeMutation: composeMutationAssertions(this.defaults.assertBeforeMutation, options.assertBeforeMutation),
|
|
263
|
+
mutationSymlinks: options.mutationSymlinks ?? this.defaults.mutationSymlinks,
|
|
217
264
|
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
218
265
|
};
|
|
219
266
|
}
|
|
220
267
|
async resolve(relativePath) {
|
|
221
268
|
assertValidRootDestinationPath(relativePath);
|
|
222
|
-
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
|
|
269
|
+
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true, resolveCanonical: true })).resolved;
|
|
223
270
|
}
|
|
224
271
|
async open(relativePath, options = {}) {
|
|
225
272
|
return await openFileInRoot(this.context, {
|
|
@@ -265,7 +312,7 @@ export class RootHandle {
|
|
|
265
312
|
...this.mutationOptions(options),
|
|
266
313
|
append: writeMode === "append",
|
|
267
314
|
truncateExisting: writeMode === "replace",
|
|
268
|
-
});
|
|
315
|
+
}).catch(rethrowMutationAuthorityError);
|
|
269
316
|
}
|
|
270
317
|
async append(relativePath, data, options = {}) {
|
|
271
318
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -276,28 +323,28 @@ export class RootHandle {
|
|
|
276
323
|
mode: this.defaults.mode,
|
|
277
324
|
...this.mutationOptions(options),
|
|
278
325
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
279
|
-
});
|
|
326
|
+
}).catch(rethrowMutationAuthorityError);
|
|
280
327
|
}
|
|
281
328
|
async remove(relativePath, options = {}) {
|
|
282
329
|
assertValidRootRelativePath(relativePath);
|
|
283
330
|
await removePathInRoot(this.context, {
|
|
284
331
|
relativePath,
|
|
285
332
|
...this.mutationOptions(options),
|
|
286
|
-
});
|
|
333
|
+
}).catch(rethrowMutationAuthorityError);
|
|
287
334
|
}
|
|
288
335
|
async mkdir(relativePath, options = {}) {
|
|
289
336
|
assertValidRootDestinationPath(relativePath);
|
|
290
337
|
await mkdirPathInRoot(this.context, {
|
|
291
338
|
relativePath,
|
|
292
339
|
...this.mutationOptions(options),
|
|
293
|
-
});
|
|
340
|
+
}).catch(rethrowMutationAuthorityError);
|
|
294
341
|
}
|
|
295
342
|
async ensureRoot(options = {}) {
|
|
296
343
|
await mkdirPathInRoot(this.context, {
|
|
297
344
|
relativePath: "",
|
|
298
345
|
allowRoot: true,
|
|
299
346
|
...this.mutationOptions(options),
|
|
300
|
-
});
|
|
347
|
+
}).catch(rethrowMutationAuthorityError);
|
|
301
348
|
}
|
|
302
349
|
async write(relativePath, data, options = {}) {
|
|
303
350
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -309,7 +356,7 @@ export class RootHandle {
|
|
|
309
356
|
renameIdentity: this.defaults.renameIdentity,
|
|
310
357
|
...this.mutationOptions(options),
|
|
311
358
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
312
|
-
});
|
|
359
|
+
}).catch(rethrowMutationAuthorityError);
|
|
313
360
|
}
|
|
314
361
|
async create(relativePath, data, options = {}) {
|
|
315
362
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -321,7 +368,7 @@ export class RootHandle {
|
|
|
321
368
|
...this.mutationOptions(options),
|
|
322
369
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
323
370
|
overwrite: false,
|
|
324
|
-
});
|
|
371
|
+
}).catch(rethrowMutationAuthorityError);
|
|
325
372
|
}
|
|
326
373
|
async writeJson(relativePath, data, options = {}) {
|
|
327
374
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
@@ -333,17 +380,20 @@ export class RootHandle {
|
|
|
333
380
|
const json = stringifyJsonDocument(data, replacer, space);
|
|
334
381
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
335
382
|
}
|
|
336
|
-
async copyIn(relativePath,
|
|
383
|
+
async copyIn(relativePath, source, options = {}) {
|
|
384
|
+
options.signal?.throwIfAborted();
|
|
337
385
|
assertValidRootDestinationPath(relativePath);
|
|
338
386
|
const { maxBytes, ...copyOptions } = this.mutationOptions(options);
|
|
339
387
|
await copyFileInRoot(this.context, {
|
|
340
|
-
|
|
388
|
+
source: typeof source === "string" ? source : { root: source.root, relativePath: source.relativePath },
|
|
341
389
|
relativePath,
|
|
342
390
|
maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
|
|
343
391
|
mkdir: this.defaults.mkdir,
|
|
344
|
-
mode: this.defaults.mode,
|
|
345
392
|
...copyOptions,
|
|
346
|
-
|
|
393
|
+
mode: options.mode ?? this.defaults.mode,
|
|
394
|
+
durable: options.durable ?? this.defaults.durable ?? true,
|
|
395
|
+
verifyPublished: options[onCopyPublication],
|
|
396
|
+
}).catch(rethrowMutationAuthorityError);
|
|
347
397
|
}
|
|
348
398
|
async exists(relativePath) {
|
|
349
399
|
try {
|
|
@@ -371,7 +421,7 @@ export class RootHandle {
|
|
|
371
421
|
assertValidRootRelativePath(fromRelative);
|
|
372
422
|
assertValidRootDestinationPath(toRelative);
|
|
373
423
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
374
|
-
const { denyMutations } = this.mutationOptions(options);
|
|
424
|
+
const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
|
|
375
425
|
await assertMoveMutationAllowed(this.context, {
|
|
376
426
|
fromRelative,
|
|
377
427
|
toRelative,
|
|
@@ -380,31 +430,25 @@ export class RootHandle {
|
|
|
380
430
|
await movePathFallback(this.context, {
|
|
381
431
|
fromRelative,
|
|
382
432
|
denyMutations,
|
|
433
|
+
assertBeforeMutation,
|
|
434
|
+
mutationSymlinks,
|
|
383
435
|
overwrite: options.overwrite ?? false,
|
|
384
436
|
toRelative,
|
|
385
|
-
});
|
|
437
|
+
}).catch(rethrowMutationAuthorityError);
|
|
386
438
|
}
|
|
387
439
|
walk(relativePath, options) {
|
|
388
440
|
assertValidRootRelativePath(relativePath);
|
|
389
|
-
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);
|
|
390
450
|
}
|
|
391
451
|
}
|
|
392
|
-
function readDefaults(defaults) {
|
|
393
|
-
return {
|
|
394
|
-
hardlinks: defaults.hardlinks,
|
|
395
|
-
maxBytes: normalizeMaxBytes(defaults.maxBytes, { defaultValue: DEFAULT_ROOT_MAX_BYTES }),
|
|
396
|
-
symlinks: defaults.symlinks,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function mergeReadOptions(defaults, options) {
|
|
400
|
-
const merged = readDefaults(defaults);
|
|
401
|
-
if (options.hardlinks !== undefined)
|
|
402
|
-
merged.hardlinks = options.hardlinks;
|
|
403
|
-
merged.maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: merged.maxBytes });
|
|
404
|
-
if (options.symlinks !== undefined)
|
|
405
|
-
merged.symlinks = options.symlinks;
|
|
406
|
-
return merged;
|
|
407
|
-
}
|
|
408
452
|
export async function root(rootDir, defaults = {}) {
|
|
409
453
|
normalizeMaxBytes(defaults.maxBytes);
|
|
410
454
|
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
@@ -413,7 +457,8 @@ async function openFileInRoot(root, params) {
|
|
|
413
457
|
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
|
|
414
458
|
allowFinalSymlink: true,
|
|
415
459
|
rejectUnsafeDeviceReads: true,
|
|
416
|
-
|
|
460
|
+
...readSymlinkResolution(params.symlinks),
|
|
461
|
+
resolveCanonical: true,
|
|
417
462
|
});
|
|
418
463
|
const { opened } = await openVerifiedLocalFile(resolved, {
|
|
419
464
|
symlinks: params.symlinks,
|
|
@@ -469,38 +514,22 @@ function buildAtomicWriteTempPath(targetPath) {
|
|
|
469
514
|
function rootWriteQueueKey(root, relativePath) {
|
|
470
515
|
return `${root.rootReal}\0${relativePath}`;
|
|
471
516
|
}
|
|
472
|
-
async function prepareRootWriteTarget(rootReal, targetPath) {
|
|
517
|
+
async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
|
|
473
518
|
const parentPath = await mkdirPathComponentsWithGuards({
|
|
474
519
|
rootReal,
|
|
475
520
|
targetPath: path.dirname(targetPath),
|
|
521
|
+
assertBeforeMutation,
|
|
476
522
|
});
|
|
477
523
|
// Continue through the guarded walk's real parent instead of re-entering
|
|
478
524
|
// the original path through a symlinked component.
|
|
479
525
|
return path.join(parentPath, path.basename(targetPath));
|
|
480
526
|
}
|
|
481
|
-
async function writeTempFileForAtomicReplace(params) {
|
|
482
|
-
const tempHandle = await fs.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
|
|
483
|
-
try {
|
|
484
|
-
if (typeof params.data === "string") {
|
|
485
|
-
await tempHandle.writeFile(params.data, params.encoding ?? "utf8");
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
await tempHandle.writeFile(params.data);
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
handle: tempHandle,
|
|
492
|
-
identity: fsSync.fstatSync(tempHandle.fd, { bigint: true }),
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
await tempHandle.close().catch(() => { });
|
|
497
|
-
throw error;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
527
|
async function resolveGuardedWritePathInRoot(root, params) {
|
|
501
528
|
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
502
529
|
aliasErrorCode: "path-alias",
|
|
530
|
+
rejectAmbiguousParents: true,
|
|
503
531
|
allowFinalSymlink: params.allowFinalSymlink,
|
|
532
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
504
533
|
});
|
|
505
534
|
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
506
535
|
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
@@ -523,12 +552,14 @@ async function openWritableFileInRoot(root, params) {
|
|
|
523
552
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
524
553
|
relativePath: params.relativePath,
|
|
525
554
|
denyMutations: params.denyMutations,
|
|
555
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
526
556
|
});
|
|
527
557
|
let ioPath = params.mkdir === false
|
|
528
558
|
? resolved
|
|
529
|
-
: await prepareRootWriteTarget(rootReal, resolved);
|
|
559
|
+
: await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
|
|
530
560
|
try {
|
|
531
|
-
|
|
561
|
+
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
562
|
+
const resolvedRealPath = params.mutationSymlinks === undefined ? fsSync.realpathSync.native(ioPath) : ioPath;
|
|
532
563
|
if (!isPathInside(rootWithSep, resolvedRealPath)) {
|
|
533
564
|
throw outsideWorkspaceError();
|
|
534
565
|
}
|
|
@@ -558,6 +589,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
558
589
|
if (!isNotFoundPathError(err)) {
|
|
559
590
|
throw err;
|
|
560
591
|
}
|
|
592
|
+
params.assertBeforeMutation?.();
|
|
561
593
|
handle = await fs.open(ioPath, createFlags, mode);
|
|
562
594
|
createdForWrite = true;
|
|
563
595
|
}
|
|
@@ -616,6 +648,8 @@ async function openWritableFileInRoot(root, params) {
|
|
|
616
648
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
617
649
|
// irreversible side effects if a symlink target changes before validation.
|
|
618
650
|
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
|
|
651
|
+
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
652
|
+
params.assertBeforeMutation?.();
|
|
619
653
|
await handle.truncate(0);
|
|
620
654
|
}
|
|
621
655
|
return {
|
|
@@ -638,42 +672,72 @@ async function openWritableFileInRoot(root, params) {
|
|
|
638
672
|
}
|
|
639
673
|
}
|
|
640
674
|
async function appendFileInRoot(root, params) {
|
|
641
|
-
const
|
|
642
|
-
relativePath: params.relativePath,
|
|
643
|
-
mkdir: params.mkdir,
|
|
644
|
-
mode: params.mode,
|
|
645
|
-
denyMutations: params.denyMutations,
|
|
646
|
-
truncateExisting: false,
|
|
647
|
-
append: true,
|
|
648
|
-
});
|
|
675
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
649
676
|
try {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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));
|
|
660
727
|
}
|
|
661
728
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
667
|
-
await target.handle.appendFile(payload);
|
|
668
|
-
}
|
|
669
|
-
if (params.durable !== false)
|
|
670
|
-
await target.handle.sync();
|
|
671
|
-
if (params.durable !== false && target.createdForWrite) {
|
|
672
|
-
await syncDirectoryBestEffort(path.dirname(target.realPath));
|
|
729
|
+
catch (error) {
|
|
730
|
+
rethrowMutationAuthorityError(error);
|
|
673
731
|
}
|
|
674
732
|
}
|
|
733
|
+
catch (e_1) {
|
|
734
|
+
env_1.error = e_1;
|
|
735
|
+
env_1.hasError = true;
|
|
736
|
+
}
|
|
675
737
|
finally {
|
|
676
|
-
|
|
738
|
+
const result_1 = __disposeResources(env_1);
|
|
739
|
+
if (result_1)
|
|
740
|
+
await result_1;
|
|
677
741
|
}
|
|
678
742
|
}
|
|
679
743
|
async function removePathInRoot(root, params) {
|
|
@@ -681,10 +745,11 @@ async function removePathInRoot(root, params) {
|
|
|
681
745
|
const resolved = await resolvePinnedPathInRoot(root, {
|
|
682
746
|
relativePath: params.relativePath,
|
|
683
747
|
denyMutations: params.denyMutations,
|
|
748
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
684
749
|
remove: true,
|
|
685
750
|
});
|
|
686
751
|
try {
|
|
687
|
-
await removePathFallback(resolved);
|
|
752
|
+
await removePathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
|
|
688
753
|
}
|
|
689
754
|
catch (error) {
|
|
690
755
|
throw normalizePinnedPathError(error);
|
|
@@ -694,7 +759,7 @@ async function mkdirPathInRoot(root, params) {
|
|
|
694
759
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
695
760
|
const resolved = await resolvePinnedPathInRoot(root, params);
|
|
696
761
|
try {
|
|
697
|
-
await mkdirPathFallback(resolved);
|
|
762
|
+
await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
|
|
698
763
|
}
|
|
699
764
|
catch (error) {
|
|
700
765
|
throw normalizePinnedPathError(error);
|
|
@@ -707,7 +772,7 @@ async function writeFileInRoot(root, params) {
|
|
|
707
772
|
await writeFileFallback(root, params);
|
|
708
773
|
return;
|
|
709
774
|
}
|
|
710
|
-
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);
|
|
711
776
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
712
777
|
await commitPinnedWriteInRoot(root, pinned, params);
|
|
713
778
|
});
|
|
@@ -726,8 +791,10 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
726
791
|
mode: params.mode ?? pinned.mode,
|
|
727
792
|
sync: params.durable !== false,
|
|
728
793
|
overwrite: params.overwrite,
|
|
794
|
+
rejectFinalSymlink: params.mutationSymlinks !== undefined,
|
|
729
795
|
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
730
796
|
rootIdentity: root.rootIdentity,
|
|
797
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
731
798
|
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
732
799
|
verifyingPublication = true;
|
|
733
800
|
try {
|
|
@@ -762,44 +829,75 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
762
829
|
}
|
|
763
830
|
}
|
|
764
831
|
async function copyFileInRoot(root, params) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
852
|
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
771
853
|
await source.handle.close().catch(() => { });
|
|
772
854
|
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
773
855
|
}
|
|
774
856
|
try {
|
|
775
857
|
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
776
|
-
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);
|
|
777
859
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
778
860
|
await assertCopySourceCurrent(source, sourceIdentity);
|
|
779
|
-
|
|
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);
|
|
780
869
|
try {
|
|
781
|
-
|
|
870
|
+
await runPinnedWriteHelper({
|
|
782
871
|
rootPath: pinned.rootReal,
|
|
783
872
|
relativeParentPath: pinned.relativeParentPath,
|
|
784
873
|
basename: pinned.basename,
|
|
785
874
|
mkdir: params.mkdir !== false,
|
|
786
875
|
mode: pinned.mode,
|
|
787
|
-
overwrite:
|
|
876
|
+
overwrite: params.overwrite !== false,
|
|
877
|
+
rejectFinalSymlink: params.mutationSymlinks !== undefined,
|
|
788
878
|
maxBytes: params.maxBytes,
|
|
789
|
-
|
|
879
|
+
sync: params.durable !== false,
|
|
880
|
+
assertBeforeMutation: () => {
|
|
881
|
+
if (params.signal?.aborted)
|
|
882
|
+
throw new MutationAuthorityError(params.signal.reason);
|
|
883
|
+
params.assertBeforeMutation?.();
|
|
884
|
+
},
|
|
885
|
+
verifyPublished: params.verifyPublished,
|
|
886
|
+
onPublished: observer.onPublished,
|
|
887
|
+
input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
|
|
790
888
|
rootIdentity: root.rootIdentity,
|
|
791
889
|
});
|
|
792
890
|
}
|
|
793
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
|
+
}
|
|
794
898
|
throw normalizePinnedWriteError(error);
|
|
795
899
|
}
|
|
796
|
-
|
|
797
|
-
await assertCopySourcePathCurrent(source, sourceIdentity);
|
|
798
|
-
}
|
|
799
|
-
catch (error) {
|
|
800
|
-
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
801
|
-
throw error;
|
|
802
|
-
}
|
|
900
|
+
await verifySource();
|
|
803
901
|
});
|
|
804
902
|
});
|
|
805
903
|
}
|
|
@@ -807,33 +905,20 @@ async function copyFileInRoot(root, params) {
|
|
|
807
905
|
await source.handle.close().catch(() => { });
|
|
808
906
|
}
|
|
809
907
|
}
|
|
810
|
-
async function assertCopySourceCurrent(source, identity) {
|
|
811
|
-
await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }), identity);
|
|
812
|
-
await assertCopySourcePathCurrent(source, identity);
|
|
813
|
-
}
|
|
814
|
-
async function assertCopySourcePathCurrent(source, identity) {
|
|
815
|
-
await inspectFileIdentity(async () => {
|
|
816
|
-
const current = fsSync.lstatSync(source.realPath, { bigint: true });
|
|
817
|
-
if (current.isSymbolicLink() || !current.isFile()) {
|
|
818
|
-
throw new FsSafeError("path-mismatch", "copy source path changed");
|
|
819
|
-
}
|
|
820
|
-
return current;
|
|
821
|
-
}, identity);
|
|
822
|
-
}
|
|
823
908
|
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
824
909
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
825
|
-
const current = fsSync.lstatSync(targetPath);
|
|
826
|
-
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity)) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
910
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
830
|
-
|
|
911
|
+
const current = fsSync.lstatSync(targetPath);
|
|
912
|
+
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
|
|
913
|
+
return;
|
|
914
|
+
await fs.unlink(targetPath);
|
|
831
915
|
});
|
|
832
916
|
}
|
|
833
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
|
|
917
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
|
|
834
918
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
835
919
|
relativePath,
|
|
836
920
|
denyMutations,
|
|
921
|
+
mutationSymlinks,
|
|
837
922
|
});
|
|
838
923
|
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
839
924
|
// is rejected upstream.
|
|
@@ -879,6 +964,7 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
879
964
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
880
965
|
allowRoot: params.allowRoot,
|
|
881
966
|
denyMutations: params.denyMutations,
|
|
967
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
882
968
|
protectDenyMutationAncestors: params.remove === true,
|
|
883
969
|
relativePath: params.relativePath,
|
|
884
970
|
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
@@ -888,6 +974,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
888
974
|
const resolved = await resolvePinnedRootPathInRoot(root, {
|
|
889
975
|
relativePath: params.relativePath,
|
|
890
976
|
policy: params.policy,
|
|
977
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
891
978
|
});
|
|
892
979
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
893
980
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
@@ -924,9 +1011,12 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
924
1011
|
rootCanonicalPath: rootReal,
|
|
925
1012
|
boundaryLabel: "root",
|
|
926
1013
|
policy: params.policy,
|
|
1014
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
927
1015
|
});
|
|
928
1016
|
}
|
|
929
1017
|
catch (err) {
|
|
1018
|
+
if (err instanceof FsSafeError && err.code === "symlink")
|
|
1019
|
+
throw err;
|
|
930
1020
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
931
1021
|
}
|
|
932
1022
|
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
@@ -947,19 +1037,23 @@ async function prepareRemoveGuard(targetPath) {
|
|
|
947
1037
|
throw normalizeRemoveGuardError(error);
|
|
948
1038
|
}
|
|
949
1039
|
}
|
|
950
|
-
async function removePathFallback(resolved) {
|
|
1040
|
+
async function removePathFallback(resolved, assertBeforeMutation, rejectFinalSymlink = false) {
|
|
951
1041
|
const guard = await prepareRemoveGuard(resolved.resolved);
|
|
952
1042
|
try {
|
|
953
|
-
|
|
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));
|
|
954
1047
|
}
|
|
955
1048
|
catch (error) {
|
|
956
1049
|
throw normalizeRemovePathError(error);
|
|
957
1050
|
}
|
|
958
|
-
await assertAsyncDirectoryGuard(guard).catch(
|
|
1051
|
+
await assertAsyncDirectoryGuard(guard).catch(error => { throw normalizeRemoveGuardError(error); });
|
|
959
1052
|
}
|
|
960
|
-
async function mkdirPathFallback(resolved) {
|
|
1053
|
+
async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
|
|
961
1054
|
await mkdirPathComponentsWithGuards({
|
|
962
|
-
rootReal: resolved.rootReal, targetPath: resolved.resolved,
|
|
1055
|
+
rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
|
|
1056
|
+
rejectSymlinks,
|
|
963
1057
|
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
964
1058
|
});
|
|
965
1059
|
}
|
|
@@ -979,31 +1073,7 @@ async function statPathFallback(root, relativePath) {
|
|
|
979
1073
|
}
|
|
980
1074
|
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
981
1075
|
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
982
|
-
|
|
983
|
-
const names = await fs.readdir(resolved.resolved);
|
|
984
|
-
const sortedNames = names.toSorted();
|
|
985
|
-
if (!withFileTypes) {
|
|
986
|
-
await assertRootIdentityCurrent(root);
|
|
987
|
-
return sortedNames;
|
|
988
|
-
}
|
|
989
|
-
const entries = [];
|
|
990
|
-
for (const name of sortedNames) {
|
|
991
|
-
entries.push({
|
|
992
|
-
name,
|
|
993
|
-
...pathStatFromStats(fsSync.lstatSync(path.join(resolved.resolved, name))),
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
await assertRootIdentityCurrent(root);
|
|
997
|
-
return entries;
|
|
998
|
-
}
|
|
999
|
-
catch (error) {
|
|
1000
|
-
if (isNotFoundPathError(error)) {
|
|
1001
|
-
throw new FsSafeError("not-found", "directory not found", {
|
|
1002
|
-
cause: error instanceof Error ? error : undefined,
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
throw error;
|
|
1006
|
-
}
|
|
1076
|
+
return await listDirectoryPath(root, resolved.resolved, withFileTypes);
|
|
1007
1077
|
}
|
|
1008
1078
|
async function assertMoveMutationAllowed(root, params) {
|
|
1009
1079
|
// Keep this preflight separate from the pinned resolutions in movePathFallback:
|
|
@@ -1023,15 +1093,18 @@ async function movePathFallback(root, params) {
|
|
|
1023
1093
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
1024
1094
|
aliasErrorCode: "path-alias",
|
|
1025
1095
|
allowFinalSymlink: true,
|
|
1096
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1026
1097
|
});
|
|
1027
1098
|
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1028
1099
|
await resolvePinnedRootPathInRoot(root, {
|
|
1029
1100
|
relativePath: params.fromRelative,
|
|
1030
1101
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1102
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1031
1103
|
});
|
|
1032
1104
|
const target = await resolveGuardedWritePathInRoot(root, {
|
|
1033
1105
|
relativePath: params.toRelative,
|
|
1034
1106
|
denyMutations: params.denyMutations,
|
|
1107
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1035
1108
|
allowFinalSymlink: true,
|
|
1036
1109
|
protectDeniedAncestors: true,
|
|
1037
1110
|
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
@@ -1088,6 +1161,9 @@ async function movePathFallback(root, params) {
|
|
|
1088
1161
|
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1089
1162
|
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1090
1163
|
try {
|
|
1164
|
+
assertFinalSymlinkRejected(source.resolved, params.mutationSymlinks !== undefined);
|
|
1165
|
+
assertFinalSymlinkRejected(target.resolved, params.mutationSymlinks !== undefined);
|
|
1166
|
+
params.assertBeforeMutation?.();
|
|
1091
1167
|
await fs.rename(source.resolved, target.resolved);
|
|
1092
1168
|
}
|
|
1093
1169
|
catch (error) {
|
|
@@ -1101,7 +1177,13 @@ async function movePathFallback(root, params) {
|
|
|
1101
1177
|
}
|
|
1102
1178
|
throw error;
|
|
1103
1179
|
}
|
|
1104
|
-
|
|
1180
|
+
try {
|
|
1181
|
+
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1182
|
+
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
throw normalizePinnedPathError(error);
|
|
1186
|
+
}
|
|
1105
1187
|
}
|
|
1106
1188
|
async function writeFileFallback(root, params) {
|
|
1107
1189
|
if (params.overwrite === false) {
|
|
@@ -1111,41 +1193,79 @@ async function writeFileFallback(root, params) {
|
|
|
1111
1193
|
const target = await openWritableFileInRoot(root, {
|
|
1112
1194
|
relativePath: params.relativePath,
|
|
1113
1195
|
mkdir: params.mkdir,
|
|
1114
|
-
|
|
1196
|
+
// Private, writable placeholder: Windows cannot rename over a read-only file.
|
|
1197
|
+
mode: 0o600,
|
|
1115
1198
|
denyMutations: params.denyMutations,
|
|
1199
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
1200
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1116
1201
|
truncateExisting: false,
|
|
1117
1202
|
});
|
|
1118
1203
|
const destinationPath = target.realPath;
|
|
1119
1204
|
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
1120
|
-
await
|
|
1121
|
-
|
|
1205
|
+
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
|
|
1206
|
+
await target.handle.close().catch(() => undefined);
|
|
1207
|
+
throw error;
|
|
1208
|
+
});
|
|
1122
1209
|
let tempPath = null;
|
|
1123
1210
|
let unregisterTempPath = null;
|
|
1124
1211
|
let writtenHandle;
|
|
1212
|
+
let writtenIdentity;
|
|
1213
|
+
let placeholderIdentity;
|
|
1214
|
+
let published = false;
|
|
1125
1215
|
try {
|
|
1216
|
+
if (target.createdForWrite)
|
|
1217
|
+
placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
|
|
1126
1218
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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,
|
|
1133
1225
|
});
|
|
1134
|
-
|
|
1135
|
-
|
|
1226
|
+
if (params.durable !== false)
|
|
1227
|
+
await writtenHandle.sync();
|
|
1136
1228
|
const commitTempPath = tempPath;
|
|
1229
|
+
const commitHandle = writtenHandle;
|
|
1230
|
+
const commitIdentity = writtenIdentity;
|
|
1137
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?.();
|
|
1138
1245
|
await fs.rename(commitTempPath, destinationPath);
|
|
1139
1246
|
tempPath = null;
|
|
1247
|
+
published = true;
|
|
1140
1248
|
});
|
|
1141
1249
|
unregisterTempPath();
|
|
1142
1250
|
unregisterTempPath = null;
|
|
1251
|
+
// Final mode via the retained handle after publication, as the native writer does.
|
|
1252
|
+
try {
|
|
1253
|
+
await writtenHandle.chmod(mode);
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
await cleanupPinnedFilePath({
|
|
1257
|
+
pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
|
|
1258
|
+
});
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
if (params.durable !== false)
|
|
1262
|
+
await writtenHandle.sync();
|
|
1143
1263
|
try {
|
|
1144
1264
|
await verifyAtomicWriteResult({
|
|
1145
1265
|
root,
|
|
1146
1266
|
targetPath: destinationPath,
|
|
1147
|
-
expectedIdentity:
|
|
1148
|
-
fd:
|
|
1267
|
+
expectedIdentity: writtenIdentity,
|
|
1268
|
+
fd: writtenHandle.fd,
|
|
1149
1269
|
parentGuard: destinationGuard,
|
|
1150
1270
|
});
|
|
1151
1271
|
}
|
|
@@ -1153,12 +1273,22 @@ async function writeFileFallback(root, params) {
|
|
|
1153
1273
|
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
1154
1274
|
throw err;
|
|
1155
1275
|
}
|
|
1276
|
+
if (params.durable !== false)
|
|
1277
|
+
await syncDirectoryBestEffort(path.dirname(destinationPath));
|
|
1156
1278
|
}
|
|
1157
1279
|
finally {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1280
|
+
if (!published && target.createdForWrite) {
|
|
1281
|
+
await cleanupPinnedFilePath({
|
|
1282
|
+
pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
|
|
1283
|
+
});
|
|
1161
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
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1162
1292
|
unregisterTempPath?.();
|
|
1163
1293
|
}
|
|
1164
1294
|
}
|
|
@@ -1166,38 +1296,38 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1166
1296
|
const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
1167
1297
|
relativePath: params.relativePath,
|
|
1168
1298
|
denyMutations: params.denyMutations,
|
|
1299
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1169
1300
|
});
|
|
1170
1301
|
const targetPath = params.mkdir === false
|
|
1171
1302
|
? resolved
|
|
1172
|
-
: await prepareRootWriteTarget(rootReal, resolved);
|
|
1173
|
-
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
1303
|
+
: await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
|
|
1304
|
+
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
|
|
1174
1305
|
let created = false;
|
|
1175
1306
|
let createdIdentity;
|
|
1176
1307
|
let writtenHandle;
|
|
1177
1308
|
let verifyingPublication = false;
|
|
1178
1309
|
try {
|
|
1179
1310
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1311
|
+
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1312
|
+
params.assertBeforeMutation?.();
|
|
1180
1313
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
|
|
1314
|
+
writtenHandle = handle;
|
|
1181
1315
|
created = true;
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
await handle.close().catch(() => undefined);
|
|
1195
|
-
throw error;
|
|
1196
|
-
}
|
|
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 };
|
|
1197
1328
|
}, {
|
|
1198
|
-
onPostGuardFailure:
|
|
1329
|
+
onPostGuardFailure: () => {
|
|
1199
1330
|
created = false; // Parent is untrusted now; skip outer path cleanup by name.
|
|
1200
|
-
await handle.close().catch(() => undefined);
|
|
1201
1331
|
},
|
|
1202
1332
|
});
|
|
1203
1333
|
writtenHandle = handle;
|
|
@@ -1210,6 +1340,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1210
1340
|
fd: handle.fd,
|
|
1211
1341
|
parentGuard,
|
|
1212
1342
|
});
|
|
1343
|
+
if (params.durable !== false)
|
|
1344
|
+
await syncDirectoryBestEffort(path.dirname(targetPath));
|
|
1213
1345
|
}
|
|
1214
1346
|
catch (err) {
|
|
1215
1347
|
if (verifyingPublication)
|
|
@@ -1222,9 +1354,11 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1222
1354
|
throw err;
|
|
1223
1355
|
}
|
|
1224
1356
|
finally {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1357
|
+
if (created && writtenHandle) {
|
|
1358
|
+
await cleanupPinnedFilePath({
|
|
1359
|
+
pathname: targetPath, handle: writtenHandle, identity: createdIdentity, parentGuard,
|
|
1360
|
+
});
|
|
1228
1361
|
}
|
|
1362
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1229
1363
|
}
|
|
1230
1364
|
}
|