@openclaw/fs-safe 0.9.0 → 0.11.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 +134 -0
- package/LICENSE +1 -0
- package/README.md +52 -4
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -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 +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -5
- package/dist/archive-gzip-tail.d.ts +3 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +23 -3
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +16 -13
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -6
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +83 -74
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- 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 +23 -10
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +18 -14
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +13 -8
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +12 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +82 -45
- 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 +22 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +69 -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 +222 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +125 -0
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +9 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +135 -39
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +6 -4
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -13
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +4 -2
- 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/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +413 -0
- 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 +5 -8
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +19 -18
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +24 -25
- 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 +56 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -34
- package/dist/mutation-authority.d.ts +9 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +29 -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 +19 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +3 -1
- 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-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +5 -2
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +85 -53
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +7 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +51 -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 +6 -4
- 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/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- 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 +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +53 -28
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +26 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +219 -0
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +6 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +16 -12
- 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 +17 -51
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +358 -234
- package/dist/root-options.d.ts +77 -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 +59 -3
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- 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-paths.d.ts.map +1 -1
- package/dist/root-paths.js +13 -9
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +14 -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 +3 -5
- package/dist/root.d.ts +4 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +25 -8
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +42 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +6 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +17 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -1
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +12 -6
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +9 -7
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -28
- 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 +7 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +26 -0
- package/docs/advanced.md +22 -4
- package/docs/archive.md +44 -9
- package/docs/atomic.md +24 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +36 -1
- package/docs/copy.md +155 -0
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +53 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +4 -4
- package/docs/file-store.md +15 -0
- package/docs/guest.md +141 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install.md +31 -0
- package/docs/local-roots.md +8 -1
- package/docs/native-helper.md +10 -3
- package/docs/native.md +18 -1
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/permissions.md +29 -10
- package/docs/positional-read.md +63 -0
- package/docs/public-api.md +31 -2
- package/docs/root.md +196 -6
- package/docs/secure-file.md +2 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +12 -3
- package/docs/temp.md +35 -6
- package/docs/timing.md +2 -0
- package/docs/types.md +19 -12
- package/docs/walk.md +54 -5
- package/docs/writing.md +173 -5
- package/package.json +19 -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,20 +71,31 @@ 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";
|
|
76
|
+
import { realpathSync } from "./realpath.js";
|
|
22
77
|
import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
|
|
23
78
|
import { resolveRootPath } from "./root-path.js";
|
|
79
|
+
import { openRootDirectoryListing, listDirectoryPath, pathStatFromStats } from "./root-directory-list.js";
|
|
80
|
+
import { entriesInRoot } from "./root-entries.js";
|
|
24
81
|
import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
|
|
25
|
-
import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError,
|
|
82
|
+
import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
|
|
26
83
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
27
84
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
28
85
|
import { walkRoot } from "./root-walk.js";
|
|
29
86
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
87
|
+
import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
|
|
30
88
|
import { serializePathWrite } from "./write-queue.js";
|
|
31
89
|
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
32
90
|
import { inheritWriteTargetMode } from "./root-write-mode.js";
|
|
33
91
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
34
|
-
import { onCopyPublication } from "./copy-publication.js";
|
|
92
|
+
import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
|
|
93
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
94
|
+
import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
|
|
95
|
+
import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
|
|
96
|
+
import { mergeReadOptions, readDefaults, } from "./root-options.js";
|
|
97
|
+
import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
|
|
98
|
+
export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
|
|
35
99
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
36
100
|
function logWarn(message) {
|
|
37
101
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
@@ -54,22 +118,6 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
54
118
|
fsConstants.O_CREAT |
|
|
55
119
|
fsConstants.O_EXCL |
|
|
56
120
|
(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
121
|
function openResult(params) {
|
|
74
122
|
return {
|
|
75
123
|
handle: params.handle,
|
|
@@ -215,12 +263,14 @@ export class RootHandle {
|
|
|
215
263
|
mutationOptions(options) {
|
|
216
264
|
return {
|
|
217
265
|
...options,
|
|
266
|
+
assertBeforeMutation: composeMutationAssertions(this.defaults.assertBeforeMutation, options.assertBeforeMutation),
|
|
267
|
+
mutationSymlinks: options.mutationSymlinks ?? this.defaults.mutationSymlinks,
|
|
218
268
|
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
219
269
|
};
|
|
220
270
|
}
|
|
221
271
|
async resolve(relativePath) {
|
|
222
272
|
assertValidRootDestinationPath(relativePath);
|
|
223
|
-
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
|
|
273
|
+
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true, resolveCanonical: true })).resolved;
|
|
224
274
|
}
|
|
225
275
|
async open(relativePath, options = {}) {
|
|
226
276
|
return await openFileInRoot(this.context, {
|
|
@@ -266,7 +316,7 @@ export class RootHandle {
|
|
|
266
316
|
...this.mutationOptions(options),
|
|
267
317
|
append: writeMode === "append",
|
|
268
318
|
truncateExisting: writeMode === "replace",
|
|
269
|
-
});
|
|
319
|
+
}).catch(rethrowMutationAuthorityError);
|
|
270
320
|
}
|
|
271
321
|
async append(relativePath, data, options = {}) {
|
|
272
322
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -277,28 +327,30 @@ export class RootHandle {
|
|
|
277
327
|
mode: this.defaults.mode,
|
|
278
328
|
...this.mutationOptions(options),
|
|
279
329
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
280
|
-
});
|
|
330
|
+
}).catch(rethrowMutationAuthorityError);
|
|
281
331
|
}
|
|
282
332
|
async remove(relativePath, options = {}) {
|
|
283
333
|
assertValidRootRelativePath(relativePath);
|
|
334
|
+
validateRemoveOptions(options);
|
|
335
|
+
options.signal?.throwIfAborted();
|
|
284
336
|
await removePathInRoot(this.context, {
|
|
285
337
|
relativePath,
|
|
286
338
|
...this.mutationOptions(options),
|
|
287
|
-
});
|
|
339
|
+
}).catch(rethrowMutationAuthorityError);
|
|
288
340
|
}
|
|
289
341
|
async mkdir(relativePath, options = {}) {
|
|
290
342
|
assertValidRootDestinationPath(relativePath);
|
|
291
343
|
await mkdirPathInRoot(this.context, {
|
|
292
344
|
relativePath,
|
|
293
345
|
...this.mutationOptions(options),
|
|
294
|
-
});
|
|
346
|
+
}).catch(rethrowMutationAuthorityError);
|
|
295
347
|
}
|
|
296
348
|
async ensureRoot(options = {}) {
|
|
297
349
|
await mkdirPathInRoot(this.context, {
|
|
298
350
|
relativePath: "",
|
|
299
351
|
allowRoot: true,
|
|
300
352
|
...this.mutationOptions(options),
|
|
301
|
-
});
|
|
353
|
+
}).catch(rethrowMutationAuthorityError);
|
|
302
354
|
}
|
|
303
355
|
async write(relativePath, data, options = {}) {
|
|
304
356
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -310,7 +362,7 @@ export class RootHandle {
|
|
|
310
362
|
renameIdentity: this.defaults.renameIdentity,
|
|
311
363
|
...this.mutationOptions(options),
|
|
312
364
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
313
|
-
});
|
|
365
|
+
}).catch(rethrowMutationAuthorityError);
|
|
314
366
|
}
|
|
315
367
|
async create(relativePath, data, options = {}) {
|
|
316
368
|
assertValidRootDestinationPath(relativePath);
|
|
@@ -319,10 +371,10 @@ export class RootHandle {
|
|
|
319
371
|
data,
|
|
320
372
|
mkdir: this.defaults.mkdir,
|
|
321
373
|
mode: this.defaults.mode,
|
|
322
|
-
...this.mutationOptions(options),
|
|
374
|
+
...this.mutationOptions(createInputOptions(data, options, this.defaults.maxBytes)),
|
|
323
375
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
324
376
|
overwrite: false,
|
|
325
|
-
});
|
|
377
|
+
}).catch(rethrowMutationAuthorityError).catch(rethrowCreateInputError);
|
|
326
378
|
}
|
|
327
379
|
async writeJson(relativePath, data, options = {}) {
|
|
328
380
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
@@ -334,19 +386,20 @@ export class RootHandle {
|
|
|
334
386
|
const json = stringifyJsonDocument(data, replacer, space);
|
|
335
387
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
336
388
|
}
|
|
337
|
-
async copyIn(relativePath,
|
|
389
|
+
async copyIn(relativePath, source, options = {}) {
|
|
390
|
+
options.signal?.throwIfAborted();
|
|
338
391
|
assertValidRootDestinationPath(relativePath);
|
|
339
392
|
const { maxBytes, ...copyOptions } = this.mutationOptions(options);
|
|
340
393
|
await copyFileInRoot(this.context, {
|
|
341
|
-
|
|
394
|
+
source: typeof source === "string" ? source : { root: source.root, relativePath: source.relativePath },
|
|
342
395
|
relativePath,
|
|
343
396
|
maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
|
|
344
397
|
mkdir: this.defaults.mkdir,
|
|
345
|
-
mode: this.defaults.mode,
|
|
346
398
|
...copyOptions,
|
|
399
|
+
mode: options.mode ?? this.defaults.mode,
|
|
347
400
|
durable: options.durable ?? this.defaults.durable ?? true,
|
|
348
401
|
verifyPublished: options[onCopyPublication],
|
|
349
|
-
});
|
|
402
|
+
}).catch(rethrowMutationAuthorityError);
|
|
350
403
|
}
|
|
351
404
|
async exists(relativePath) {
|
|
352
405
|
try {
|
|
@@ -374,7 +427,7 @@ export class RootHandle {
|
|
|
374
427
|
assertValidRootRelativePath(fromRelative);
|
|
375
428
|
assertValidRootDestinationPath(toRelative);
|
|
376
429
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
377
|
-
const { denyMutations } = this.mutationOptions(options);
|
|
430
|
+
const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
|
|
378
431
|
await assertMoveMutationAllowed(this.context, {
|
|
379
432
|
fromRelative,
|
|
380
433
|
toRelative,
|
|
@@ -383,40 +436,53 @@ export class RootHandle {
|
|
|
383
436
|
await movePathFallback(this.context, {
|
|
384
437
|
fromRelative,
|
|
385
438
|
denyMutations,
|
|
439
|
+
assertBeforeMutation,
|
|
440
|
+
mutationSymlinks,
|
|
386
441
|
overwrite: options.overwrite ?? false,
|
|
387
442
|
toRelative,
|
|
443
|
+
}).catch(rethrowMutationAuthorityError);
|
|
444
|
+
}
|
|
445
|
+
entries(relativePath, options = {}) {
|
|
446
|
+
assertValidRootRelativePath(relativePath);
|
|
447
|
+
return entriesInRoot(this.context, relativePath, {
|
|
448
|
+
...options,
|
|
449
|
+
symlinks: options.symlinks ?? this.defaults.symlinks,
|
|
388
450
|
});
|
|
389
451
|
}
|
|
390
452
|
walk(relativePath, options) {
|
|
391
453
|
assertValidRootRelativePath(relativePath);
|
|
392
|
-
return walkRoot(
|
|
454
|
+
return walkRoot({
|
|
455
|
+
rootReal: this.rootReal,
|
|
456
|
+
stat: relative => this.stat(relative),
|
|
457
|
+
list: async (relative, listingOptions) => {
|
|
458
|
+
validatePinnedOperationPayload({ relativePath: relative });
|
|
459
|
+
const resolved = await resolvePinnedPathInRoot(this.context, { relativePath: relative, allowRoot: true });
|
|
460
|
+
return await openRootDirectoryListing(this.context, resolved.resolved, listingOptions);
|
|
461
|
+
},
|
|
462
|
+
}, relativePath, options);
|
|
393
463
|
}
|
|
394
464
|
}
|
|
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
465
|
export async function root(rootDir, defaults = {}) {
|
|
412
466
|
normalizeMaxBytes(defaults.maxBytes);
|
|
413
467
|
return new RootHandle(await resolveRootContext(rootDir), defaults);
|
|
414
468
|
}
|
|
469
|
+
// Internal callers that already hold an exact admitted directory capability must
|
|
470
|
+
// not recapture a different filesystem object while constructing the Root.
|
|
471
|
+
export function rootFromDirectoryGuard(guard, defaults = {}) {
|
|
472
|
+
normalizeMaxBytes(defaults.maxBytes);
|
|
473
|
+
return new RootHandle({
|
|
474
|
+
rootDir: path.resolve(guard.dir),
|
|
475
|
+
rootIdentity: { dev: guard.stat.dev, ino: guard.stat.ino },
|
|
476
|
+
rootReal: guard.realPath,
|
|
477
|
+
rootWithSep: ensureTrailingSep(guard.realPath),
|
|
478
|
+
}, defaults);
|
|
479
|
+
}
|
|
415
480
|
async function openFileInRoot(root, params) {
|
|
416
481
|
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
|
|
417
482
|
allowFinalSymlink: true,
|
|
418
483
|
rejectUnsafeDeviceReads: true,
|
|
419
|
-
|
|
484
|
+
...readSymlinkResolution(params.symlinks),
|
|
485
|
+
resolveCanonical: true,
|
|
420
486
|
});
|
|
421
487
|
const { opened } = await openVerifiedLocalFile(resolved, {
|
|
422
488
|
symlinks: params.symlinks,
|
|
@@ -472,38 +538,22 @@ function buildAtomicWriteTempPath(targetPath) {
|
|
|
472
538
|
function rootWriteQueueKey(root, relativePath) {
|
|
473
539
|
return `${root.rootReal}\0${relativePath}`;
|
|
474
540
|
}
|
|
475
|
-
async function prepareRootWriteTarget(rootReal, targetPath) {
|
|
541
|
+
async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
|
|
476
542
|
const parentPath = await mkdirPathComponentsWithGuards({
|
|
477
543
|
rootReal,
|
|
478
544
|
targetPath: path.dirname(targetPath),
|
|
545
|
+
assertBeforeMutation,
|
|
479
546
|
});
|
|
480
547
|
// Continue through the guarded walk's real parent instead of re-entering
|
|
481
548
|
// the original path through a symlinked component.
|
|
482
549
|
return path.join(parentPath, path.basename(targetPath));
|
|
483
550
|
}
|
|
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
551
|
async function resolveGuardedWritePathInRoot(root, params) {
|
|
504
552
|
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
505
553
|
aliasErrorCode: "path-alias",
|
|
554
|
+
rejectAmbiguousParents: true,
|
|
506
555
|
allowFinalSymlink: params.allowFinalSymlink,
|
|
556
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
507
557
|
});
|
|
508
558
|
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
509
559
|
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
@@ -526,12 +576,14 @@ async function openWritableFileInRoot(root, params) {
|
|
|
526
576
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
527
577
|
relativePath: params.relativePath,
|
|
528
578
|
denyMutations: params.denyMutations,
|
|
579
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
529
580
|
});
|
|
530
581
|
let ioPath = params.mkdir === false
|
|
531
582
|
? resolved
|
|
532
|
-
: await prepareRootWriteTarget(rootReal, resolved);
|
|
583
|
+
: await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
|
|
533
584
|
try {
|
|
534
|
-
|
|
585
|
+
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
586
|
+
const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
|
|
535
587
|
if (!isPathInside(rootWithSep, resolvedRealPath)) {
|
|
536
588
|
throw outsideWorkspaceError();
|
|
537
589
|
}
|
|
@@ -561,6 +613,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
561
613
|
if (!isNotFoundPathError(err)) {
|
|
562
614
|
throw err;
|
|
563
615
|
}
|
|
616
|
+
params.assertBeforeMutation?.();
|
|
564
617
|
handle = await fs.open(ioPath, createFlags, mode);
|
|
565
618
|
createdForWrite = true;
|
|
566
619
|
}
|
|
@@ -619,6 +672,8 @@ async function openWritableFileInRoot(root, params) {
|
|
|
619
672
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
620
673
|
// irreversible side effects if a symlink target changes before validation.
|
|
621
674
|
if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
|
|
675
|
+
assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
|
|
676
|
+
params.assertBeforeMutation?.();
|
|
622
677
|
await handle.truncate(0);
|
|
623
678
|
}
|
|
624
679
|
return {
|
|
@@ -641,42 +696,72 @@ async function openWritableFileInRoot(root, params) {
|
|
|
641
696
|
}
|
|
642
697
|
}
|
|
643
698
|
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
|
-
});
|
|
699
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
652
700
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
701
|
+
const target = await openWritableFileInRoot(root, {
|
|
702
|
+
relativePath: params.relativePath,
|
|
703
|
+
mkdir: params.mkdir,
|
|
704
|
+
mode: params.mode,
|
|
705
|
+
denyMutations: params.denyMutations,
|
|
706
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
707
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
708
|
+
truncateExisting: false,
|
|
709
|
+
append: true,
|
|
710
|
+
});
|
|
711
|
+
let dispatched = false;
|
|
712
|
+
// Reverse disposal order closes the handle before path cleanup and retains both failures.
|
|
713
|
+
const cleanup = __addDisposableResource(env_1, {
|
|
714
|
+
async [Symbol.asyncDispose]() {
|
|
715
|
+
if (!dispatched && target.createdForWrite) {
|
|
716
|
+
await removePathIfIdentityUnchanged(target.realPath, target.stat);
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
}, true);
|
|
720
|
+
const handle = __addDisposableResource(env_1, target.handle, true);
|
|
721
|
+
try {
|
|
722
|
+
let prefix = "";
|
|
723
|
+
if (params.prependNewlineIfNeeded === true &&
|
|
724
|
+
!target.createdForWrite &&
|
|
725
|
+
target.stat.size > 0 &&
|
|
726
|
+
((typeof params.data === "string" && !params.data.startsWith("\n")) ||
|
|
727
|
+
(Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 0x0a))) {
|
|
728
|
+
const newline = Buffer.from("\n", typeof params.data === "string" ? params.encoding : "utf8");
|
|
729
|
+
const tail = Buffer.alloc(newline.length);
|
|
730
|
+
const { bytesRead } = await target.handle.read(tail, 0, tail.length, Math.max(0, target.stat.size - tail.length));
|
|
731
|
+
if (bytesRead > 0 && (bytesRead !== newline.length || !tail.equals(newline))) {
|
|
732
|
+
prefix = "\n";
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const payload = typeof params.data === "string" ? `${prefix}${params.data}`
|
|
736
|
+
: prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
737
|
+
await writeAllToFile(target.handle, payload, {
|
|
738
|
+
encoding: params.encoding,
|
|
739
|
+
assertBeforeMutation: () => {
|
|
740
|
+
assertFinalSymlinkRejected(target.realPath, params.mutationSymlinks !== undefined);
|
|
741
|
+
params.assertBeforeMutation?.();
|
|
742
|
+
dispatched = true;
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
// A successful empty append still creates the file, as Node's appendFile does.
|
|
746
|
+
dispatched = true;
|
|
747
|
+
if (params.durable !== false)
|
|
748
|
+
await target.handle.sync();
|
|
749
|
+
if (params.durable !== false && target.createdForWrite) {
|
|
750
|
+
await syncDirectoryBestEffort(path.dirname(target.realPath));
|
|
663
751
|
}
|
|
664
752
|
}
|
|
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));
|
|
753
|
+
catch (error) {
|
|
754
|
+
rethrowMutationAuthorityError(error);
|
|
676
755
|
}
|
|
677
756
|
}
|
|
757
|
+
catch (e_1) {
|
|
758
|
+
env_1.error = e_1;
|
|
759
|
+
env_1.hasError = true;
|
|
760
|
+
}
|
|
678
761
|
finally {
|
|
679
|
-
|
|
762
|
+
const result_1 = __disposeResources(env_1);
|
|
763
|
+
if (result_1)
|
|
764
|
+
await result_1;
|
|
680
765
|
}
|
|
681
766
|
}
|
|
682
767
|
async function removePathInRoot(root, params) {
|
|
@@ -684,12 +769,15 @@ async function removePathInRoot(root, params) {
|
|
|
684
769
|
const resolved = await resolvePinnedPathInRoot(root, {
|
|
685
770
|
relativePath: params.relativePath,
|
|
686
771
|
denyMutations: params.denyMutations,
|
|
772
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
687
773
|
remove: true,
|
|
688
774
|
});
|
|
689
775
|
try {
|
|
690
|
-
await
|
|
776
|
+
await removePathInRootFallback(root, resolved.resolved, params);
|
|
691
777
|
}
|
|
692
778
|
catch (error) {
|
|
779
|
+
if (params.recursive)
|
|
780
|
+
throw error;
|
|
693
781
|
throw normalizePinnedPathError(error);
|
|
694
782
|
}
|
|
695
783
|
}
|
|
@@ -697,26 +785,27 @@ async function mkdirPathInRoot(root, params) {
|
|
|
697
785
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
698
786
|
const resolved = await resolvePinnedPathInRoot(root, params);
|
|
699
787
|
try {
|
|
700
|
-
await mkdirPathFallback(resolved);
|
|
788
|
+
await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
|
|
701
789
|
}
|
|
702
790
|
catch (error) {
|
|
703
791
|
throw normalizePinnedPathError(error);
|
|
704
792
|
}
|
|
705
793
|
}
|
|
706
794
|
async function writeFileInRoot(root, params) {
|
|
795
|
+
const input = rootWriteInput(params);
|
|
707
796
|
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
708
|
-
if (process.platform === "win32" &&
|
|
797
|
+
if (input.kind === "buffer" && process.platform === "win32" &&
|
|
709
798
|
(params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
|
|
710
|
-
await writeFileFallback(root, params);
|
|
799
|
+
await writeFileFallback(root, { ...params, data: input.data });
|
|
711
800
|
return;
|
|
712
801
|
}
|
|
713
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite);
|
|
802
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite, params.mutationSymlinks);
|
|
714
803
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
715
|
-
await commitPinnedWriteInRoot(root, pinned, params);
|
|
804
|
+
await commitPinnedWriteInRoot(root, pinned, params, input);
|
|
716
805
|
});
|
|
717
806
|
});
|
|
718
807
|
}
|
|
719
|
-
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
808
|
+
async function commitPinnedWriteInRoot(root, pinned, params, input) {
|
|
720
809
|
let verifyingPublication = false;
|
|
721
810
|
try {
|
|
722
811
|
await runPinnedWriteWithRenamePolicy({
|
|
@@ -729,8 +818,11 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
729
818
|
mode: params.mode ?? pinned.mode,
|
|
730
819
|
sync: params.durable !== false,
|
|
731
820
|
overwrite: params.overwrite,
|
|
732
|
-
|
|
821
|
+
rejectFinalSymlink: params.mutationSymlinks !== undefined,
|
|
822
|
+
input,
|
|
823
|
+
maxBytes: params.maxBytes,
|
|
733
824
|
rootIdentity: root.rootIdentity,
|
|
825
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
734
826
|
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
735
827
|
verifyingPublication = true;
|
|
736
828
|
try {
|
|
@@ -765,46 +857,75 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
765
857
|
}
|
|
766
858
|
}
|
|
767
859
|
async function copyFileInRoot(root, params) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
860
|
+
params.signal?.throwIfAborted();
|
|
861
|
+
const clone = resolveFileCopyCloneMode(params.clone);
|
|
862
|
+
let source;
|
|
863
|
+
let sourceIdentity;
|
|
864
|
+
if (typeof params.source === "string") {
|
|
865
|
+
assertNoNulPathInput(params.source, "source path contains a NUL byte");
|
|
866
|
+
({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
|
|
867
|
+
hardlinks: params.sourceHardlinks,
|
|
868
|
+
}));
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
source = await params.source.root.open(params.source.relativePath, params.sourceHardlinks === undefined ? undefined : { hardlinks: params.sourceHardlinks });
|
|
872
|
+
try {
|
|
873
|
+
sourceIdentity = await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }));
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
await source.handle.close().catch(() => undefined);
|
|
877
|
+
throw error;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
773
880
|
if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
|
|
774
881
|
await source.handle.close().catch(() => { });
|
|
775
882
|
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
776
883
|
}
|
|
777
884
|
try {
|
|
778
885
|
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
779
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
886
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode ?? (params.preserveSourceMode ? Number(sourceIdentity.mode & 4095n) : undefined), params.denyMutations, params.overwrite !== false, params.mutationSymlinks);
|
|
780
887
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
781
888
|
await assertCopySourceCurrent(source, sourceIdentity);
|
|
782
|
-
|
|
889
|
+
const verifySource = async () => {
|
|
890
|
+
params.signal?.throwIfAborted();
|
|
891
|
+
if (typeof params.source !== "string") {
|
|
892
|
+
await params.source.root.stat(".");
|
|
893
|
+
}
|
|
894
|
+
await assertCopySourceCurrent(source, sourceIdentity);
|
|
895
|
+
};
|
|
896
|
+
const observer = createCopyPublicationObserver(pinned.targetPath, params.onDestinationPublished);
|
|
783
897
|
try {
|
|
784
|
-
|
|
898
|
+
await runPinnedWriteHelper({
|
|
785
899
|
rootPath: pinned.rootReal,
|
|
786
900
|
relativeParentPath: pinned.relativeParentPath,
|
|
787
901
|
basename: pinned.basename,
|
|
788
902
|
mkdir: params.mkdir !== false,
|
|
789
903
|
mode: pinned.mode,
|
|
790
|
-
overwrite:
|
|
904
|
+
overwrite: params.overwrite !== false,
|
|
905
|
+
rejectFinalSymlink: params.mutationSymlinks !== undefined,
|
|
791
906
|
maxBytes: params.maxBytes,
|
|
792
907
|
sync: params.durable !== false,
|
|
908
|
+
assertBeforeMutation: () => {
|
|
909
|
+
if (params.signal?.aborted)
|
|
910
|
+
throw new MutationAuthorityError(params.signal.reason);
|
|
911
|
+
params.assertBeforeMutation?.();
|
|
912
|
+
},
|
|
793
913
|
verifyPublished: params.verifyPublished,
|
|
794
|
-
|
|
914
|
+
onPublished: observer.onPublished,
|
|
915
|
+
input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
|
|
795
916
|
rootIdentity: root.rootIdentity,
|
|
796
917
|
});
|
|
797
918
|
}
|
|
798
919
|
catch (error) {
|
|
920
|
+
observer.rethrowObserverFailure(error);
|
|
921
|
+
if (params.signal?.aborted && error === params.signal.reason)
|
|
922
|
+
throw error;
|
|
923
|
+
if (isAlreadyExistsError(error)) {
|
|
924
|
+
throw new FsSafeError("already-exists", "copy destination already exists", { cause: error });
|
|
925
|
+
}
|
|
799
926
|
throw normalizePinnedWriteError(error);
|
|
800
927
|
}
|
|
801
|
-
|
|
802
|
-
await assertCopySourcePathCurrent(source, sourceIdentity);
|
|
803
|
-
}
|
|
804
|
-
catch (error) {
|
|
805
|
-
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
806
|
-
throw error;
|
|
807
|
-
}
|
|
928
|
+
await verifySource();
|
|
808
929
|
});
|
|
809
930
|
});
|
|
810
931
|
}
|
|
@@ -812,33 +933,20 @@ async function copyFileInRoot(root, params) {
|
|
|
812
933
|
await source.handle.close().catch(() => { });
|
|
813
934
|
}
|
|
814
935
|
}
|
|
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
936
|
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
829
937
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
830
|
-
const current = fsSync.lstatSync(targetPath);
|
|
831
|
-
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity)) {
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
938
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
835
|
-
|
|
939
|
+
const current = fsSync.lstatSync(targetPath);
|
|
940
|
+
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
|
|
941
|
+
return;
|
|
942
|
+
await fs.unlink(targetPath);
|
|
836
943
|
});
|
|
837
944
|
}
|
|
838
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
|
|
945
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
|
|
839
946
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
840
947
|
relativePath,
|
|
841
948
|
denyMutations,
|
|
949
|
+
mutationSymlinks,
|
|
842
950
|
});
|
|
843
951
|
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
844
952
|
// is rejected upstream.
|
|
@@ -884,6 +992,7 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
884
992
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
885
993
|
allowRoot: params.allowRoot,
|
|
886
994
|
denyMutations: params.denyMutations,
|
|
995
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
887
996
|
protectDenyMutationAncestors: params.remove === true,
|
|
888
997
|
relativePath: params.relativePath,
|
|
889
998
|
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
@@ -893,6 +1002,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
893
1002
|
const resolved = await resolvePinnedRootPathInRoot(root, {
|
|
894
1003
|
relativePath: params.relativePath,
|
|
895
1004
|
policy: params.policy,
|
|
1005
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
896
1006
|
});
|
|
897
1007
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
898
1008
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
@@ -929,9 +1039,12 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
929
1039
|
rootCanonicalPath: rootReal,
|
|
930
1040
|
boundaryLabel: "root",
|
|
931
1041
|
policy: params.policy,
|
|
1042
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
932
1043
|
});
|
|
933
1044
|
}
|
|
934
1045
|
catch (err) {
|
|
1046
|
+
if (err instanceof FsSafeError && err.code === "symlink")
|
|
1047
|
+
throw err;
|
|
935
1048
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
936
1049
|
}
|
|
937
1050
|
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
@@ -941,30 +1054,10 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
941
1054
|
canonicalPath: resolved.canonicalPath,
|
|
942
1055
|
};
|
|
943
1056
|
}
|
|
944
|
-
async function
|
|
945
|
-
try {
|
|
946
|
-
const guard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
947
|
-
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
|
|
948
|
-
await assertAsyncDirectoryGuard(guard);
|
|
949
|
-
return guard;
|
|
950
|
-
}
|
|
951
|
-
catch (error) {
|
|
952
|
-
throw normalizeRemoveGuardError(error);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
async function removePathFallback(resolved) {
|
|
956
|
-
const guard = await prepareRemoveGuard(resolved.resolved);
|
|
957
|
-
try {
|
|
958
|
-
await (fsSync.lstatSync(resolved.resolved).isDirectory() ? fs.rmdir(resolved.resolved) : fs.rm(resolved.resolved));
|
|
959
|
-
}
|
|
960
|
-
catch (error) {
|
|
961
|
-
throw normalizeRemovePathError(error);
|
|
962
|
-
}
|
|
963
|
-
await assertAsyncDirectoryGuard(guard).catch(() => undefined);
|
|
964
|
-
}
|
|
965
|
-
async function mkdirPathFallback(resolved) {
|
|
1057
|
+
async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
|
|
966
1058
|
await mkdirPathComponentsWithGuards({
|
|
967
|
-
rootReal: resolved.rootReal, targetPath: resolved.resolved,
|
|
1059
|
+
rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
|
|
1060
|
+
rejectSymlinks,
|
|
968
1061
|
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
969
1062
|
});
|
|
970
1063
|
}
|
|
@@ -984,31 +1077,7 @@ async function statPathFallback(root, relativePath) {
|
|
|
984
1077
|
}
|
|
985
1078
|
async function listPathFallback(root, relativePath, withFileTypes) {
|
|
986
1079
|
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
|
-
}
|
|
1080
|
+
return await listDirectoryPath(root, resolved.resolved, withFileTypes);
|
|
1012
1081
|
}
|
|
1013
1082
|
async function assertMoveMutationAllowed(root, params) {
|
|
1014
1083
|
// Keep this preflight separate from the pinned resolutions in movePathFallback:
|
|
@@ -1028,15 +1097,18 @@ async function movePathFallback(root, params) {
|
|
|
1028
1097
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
1029
1098
|
aliasErrorCode: "path-alias",
|
|
1030
1099
|
allowFinalSymlink: true,
|
|
1100
|
+
...mutationSymlinkResolution(params.mutationSymlinks),
|
|
1031
1101
|
});
|
|
1032
1102
|
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1033
1103
|
await resolvePinnedRootPathInRoot(root, {
|
|
1034
1104
|
relativePath: params.fromRelative,
|
|
1035
1105
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1106
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1036
1107
|
});
|
|
1037
1108
|
const target = await resolveGuardedWritePathInRoot(root, {
|
|
1038
1109
|
relativePath: params.toRelative,
|
|
1039
1110
|
denyMutations: params.denyMutations,
|
|
1111
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1040
1112
|
allowFinalSymlink: true,
|
|
1041
1113
|
protectDeniedAncestors: true,
|
|
1042
1114
|
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
@@ -1093,6 +1165,9 @@ async function movePathFallback(root, params) {
|
|
|
1093
1165
|
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1094
1166
|
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1095
1167
|
try {
|
|
1168
|
+
assertFinalSymlinkRejected(source.resolved, params.mutationSymlinks !== undefined);
|
|
1169
|
+
assertFinalSymlinkRejected(target.resolved, params.mutationSymlinks !== undefined);
|
|
1170
|
+
params.assertBeforeMutation?.();
|
|
1096
1171
|
await fs.rename(source.resolved, target.resolved);
|
|
1097
1172
|
}
|
|
1098
1173
|
catch (error) {
|
|
@@ -1106,7 +1181,13 @@ async function movePathFallback(root, params) {
|
|
|
1106
1181
|
}
|
|
1107
1182
|
throw error;
|
|
1108
1183
|
}
|
|
1109
|
-
|
|
1184
|
+
try {
|
|
1185
|
+
await assertAsyncDirectoryGuard(sourceParentGuard);
|
|
1186
|
+
await assertAsyncDirectoryGuard(targetParentGuard);
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
throw normalizePinnedPathError(error);
|
|
1190
|
+
}
|
|
1110
1191
|
}
|
|
1111
1192
|
async function writeFileFallback(root, params) {
|
|
1112
1193
|
if (params.overwrite === false) {
|
|
@@ -1119,47 +1200,76 @@ async function writeFileFallback(root, params) {
|
|
|
1119
1200
|
// Private, writable placeholder: Windows cannot rename over a read-only file.
|
|
1120
1201
|
mode: 0o600,
|
|
1121
1202
|
denyMutations: params.denyMutations,
|
|
1203
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
1204
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1122
1205
|
truncateExisting: false,
|
|
1123
1206
|
});
|
|
1124
1207
|
const destinationPath = target.realPath;
|
|
1125
1208
|
const mode = params.mode ?? (target.stat.mode & 0o777);
|
|
1126
|
-
await
|
|
1127
|
-
|
|
1209
|
+
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
|
|
1210
|
+
await target.handle.close().catch(() => undefined);
|
|
1211
|
+
throw error;
|
|
1212
|
+
});
|
|
1128
1213
|
let tempPath = null;
|
|
1129
1214
|
let unregisterTempPath = null;
|
|
1130
1215
|
let writtenHandle;
|
|
1216
|
+
let writtenIdentity;
|
|
1217
|
+
let placeholderIdentity;
|
|
1218
|
+
let published = false;
|
|
1131
1219
|
try {
|
|
1220
|
+
if (target.createdForWrite)
|
|
1221
|
+
placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
|
|
1132
1222
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1223
|
+
params.assertBeforeMutation?.();
|
|
1224
|
+
writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
|
|
1225
|
+
writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
|
|
1226
|
+
unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
|
|
1227
|
+
await writeAllToFile(writtenHandle, params.data, {
|
|
1228
|
+
encoding: params.encoding, assertBeforeMutation: params.assertBeforeMutation,
|
|
1139
1229
|
});
|
|
1140
|
-
|
|
1141
|
-
|
|
1230
|
+
if (params.durable !== false)
|
|
1231
|
+
await writtenHandle.sync();
|
|
1142
1232
|
const commitTempPath = tempPath;
|
|
1233
|
+
const commitHandle = writtenHandle;
|
|
1234
|
+
const commitIdentity = writtenIdentity;
|
|
1143
1235
|
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
1236
|
+
await verifyAtomicWriteResult({
|
|
1237
|
+
root, targetPath: commitTempPath, fd: commitHandle.fd,
|
|
1238
|
+
expectedIdentity: commitIdentity, parentGuard: destinationGuard,
|
|
1239
|
+
});
|
|
1240
|
+
if (target.createdForWrite) {
|
|
1241
|
+
await cleanupPinnedFilePath({
|
|
1242
|
+
pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
// Windows cannot replace a destination while its old handle remains open.
|
|
1246
|
+
await target.handle.close();
|
|
1247
|
+
assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
|
|
1248
|
+
params.assertBeforeMutation?.();
|
|
1144
1249
|
await fs.rename(commitTempPath, destinationPath);
|
|
1145
1250
|
tempPath = null;
|
|
1251
|
+
published = true;
|
|
1146
1252
|
});
|
|
1147
1253
|
unregisterTempPath();
|
|
1148
1254
|
unregisterTempPath = null;
|
|
1149
1255
|
// Final mode via the retained handle after publication, as the native writer does.
|
|
1150
1256
|
try {
|
|
1151
|
-
await
|
|
1257
|
+
await writtenHandle.chmod(mode);
|
|
1152
1258
|
}
|
|
1153
1259
|
catch (error) {
|
|
1154
|
-
await
|
|
1260
|
+
await cleanupPinnedFilePath({
|
|
1261
|
+
pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
|
|
1262
|
+
});
|
|
1155
1263
|
throw error;
|
|
1156
1264
|
}
|
|
1265
|
+
if (params.durable !== false)
|
|
1266
|
+
await writtenHandle.sync();
|
|
1157
1267
|
try {
|
|
1158
1268
|
await verifyAtomicWriteResult({
|
|
1159
1269
|
root,
|
|
1160
1270
|
targetPath: destinationPath,
|
|
1161
|
-
expectedIdentity:
|
|
1162
|
-
fd:
|
|
1271
|
+
expectedIdentity: writtenIdentity,
|
|
1272
|
+
fd: writtenHandle.fd,
|
|
1163
1273
|
parentGuard: destinationGuard,
|
|
1164
1274
|
});
|
|
1165
1275
|
}
|
|
@@ -1167,12 +1277,22 @@ async function writeFileFallback(root, params) {
|
|
|
1167
1277
|
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
1168
1278
|
throw err;
|
|
1169
1279
|
}
|
|
1280
|
+
if (params.durable !== false)
|
|
1281
|
+
await syncDirectoryBestEffort(path.dirname(destinationPath));
|
|
1170
1282
|
}
|
|
1171
1283
|
finally {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1284
|
+
if (!published && target.createdForWrite) {
|
|
1285
|
+
await cleanupPinnedFilePath({
|
|
1286
|
+
pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
await target.handle.close().catch(() => undefined);
|
|
1290
|
+
if (tempPath && writtenHandle) {
|
|
1291
|
+
await cleanupPinnedFilePath({
|
|
1292
|
+
pathname: tempPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
|
|
1293
|
+
});
|
|
1175
1294
|
}
|
|
1295
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1176
1296
|
unregisterTempPath?.();
|
|
1177
1297
|
}
|
|
1178
1298
|
}
|
|
@@ -1180,38 +1300,38 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1180
1300
|
const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
1181
1301
|
relativePath: params.relativePath,
|
|
1182
1302
|
denyMutations: params.denyMutations,
|
|
1303
|
+
mutationSymlinks: params.mutationSymlinks,
|
|
1183
1304
|
});
|
|
1184
1305
|
const targetPath = params.mkdir === false
|
|
1185
1306
|
? resolved
|
|
1186
|
-
: await prepareRootWriteTarget(rootReal, resolved);
|
|
1187
|
-
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
1307
|
+
: await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
|
|
1308
|
+
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
|
|
1188
1309
|
let created = false;
|
|
1189
1310
|
let createdIdentity;
|
|
1190
1311
|
let writtenHandle;
|
|
1191
1312
|
let verifyingPublication = false;
|
|
1192
1313
|
try {
|
|
1193
1314
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1315
|
+
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1316
|
+
params.assertBeforeMutation?.();
|
|
1194
1317
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
|
|
1318
|
+
writtenHandle = handle;
|
|
1195
1319
|
created = true;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
await handle.close().catch(() => undefined);
|
|
1209
|
-
throw error;
|
|
1210
|
-
}
|
|
1320
|
+
const writtenStat = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
1321
|
+
createdIdentity = writtenStat;
|
|
1322
|
+
await writeAllToFile(handle, params.data, {
|
|
1323
|
+
encoding: params.encoding,
|
|
1324
|
+
assertBeforeMutation: () => {
|
|
1325
|
+
assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
|
|
1326
|
+
params.assertBeforeMutation?.();
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
if (params.durable !== false)
|
|
1330
|
+
await handle.sync();
|
|
1331
|
+
return { handle, writtenStat };
|
|
1211
1332
|
}, {
|
|
1212
|
-
onPostGuardFailure:
|
|
1333
|
+
onPostGuardFailure: () => {
|
|
1213
1334
|
created = false; // Parent is untrusted now; skip outer path cleanup by name.
|
|
1214
|
-
await handle.close().catch(() => undefined);
|
|
1215
1335
|
},
|
|
1216
1336
|
});
|
|
1217
1337
|
writtenHandle = handle;
|
|
@@ -1224,6 +1344,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1224
1344
|
fd: handle.fd,
|
|
1225
1345
|
parentGuard,
|
|
1226
1346
|
});
|
|
1347
|
+
if (params.durable !== false)
|
|
1348
|
+
await syncDirectoryBestEffort(path.dirname(targetPath));
|
|
1227
1349
|
}
|
|
1228
1350
|
catch (err) {
|
|
1229
1351
|
if (verifyingPublication)
|
|
@@ -1236,9 +1358,11 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1236
1358
|
throw err;
|
|
1237
1359
|
}
|
|
1238
1360
|
finally {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1361
|
+
if (created && writtenHandle) {
|
|
1362
|
+
await cleanupPinnedFilePath({
|
|
1363
|
+
pathname: targetPath, handle: writtenHandle, identity: createdIdentity, parentGuard,
|
|
1364
|
+
});
|
|
1242
1365
|
}
|
|
1366
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1243
1367
|
}
|
|
1244
1368
|
}
|