@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { getNativeBinding } from "./native.js";
|
|
3
|
-
import { executePermissionCommand
|
|
3
|
+
import { executePermissionCommand } from "./permission-exec.js";
|
|
4
|
+
import { safeStat } from "./permissions.js";
|
|
4
5
|
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
|
|
5
6
|
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
6
|
-
import { inspectWindowsOwner
|
|
7
|
+
import { inspectWindowsOwner } from "./windows-owner.js";
|
|
7
8
|
const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
|
|
8
9
|
const WORLD_PRINCIPALS = new Set(["everyone", "users", "builtin\\users", "authenticated users", "nt authority\\authenticated users", "anonymous logon", "nt authority\\anonymous logon", "guests", "builtin\\guests", "interactive", "nt authority\\interactive", "network", "nt authority\\network", "local"]);
|
|
9
10
|
const TRUSTED_BASE = new Set([
|
|
@@ -92,13 +93,7 @@ export async function inspectWindowsPermissions(params) {
|
|
|
92
93
|
const error = `Windows owner inspection failed: ${owner.error}`;
|
|
93
94
|
return { ...unverified, ownerError: owner.error, error, errorDetail: owner.errorDetail, errorCause: owner.errorCause };
|
|
94
95
|
}
|
|
95
|
-
const acl =
|
|
96
|
-
env: params.opts?.env,
|
|
97
|
-
exec: params.opts?.exec,
|
|
98
|
-
currentUserSid: owner.currentUserSid,
|
|
99
|
-
principalSids: owner.principalSids,
|
|
100
|
-
principalTranslationFailed: owner.principalTranslationFailed,
|
|
101
|
-
});
|
|
96
|
+
const acl = summarizeWindowsOwnerAcl(owner);
|
|
102
97
|
const ownerFields = {
|
|
103
98
|
...(owner.sid ? { ownerSid: owner.sid } : {}),
|
|
104
99
|
...(owner.trusted !== undefined ? { ownerTrusted: owner.trusted } : {}),
|
|
@@ -132,8 +127,18 @@ export function resolveWindowsUserPrincipal(env, userInfo = defaultWindowsUserIn
|
|
|
132
127
|
const domain = env?.USERDOMAIN?.trim();
|
|
133
128
|
return domain ? `${domain}\\${username}` : username;
|
|
134
129
|
}
|
|
130
|
+
function buildTrustedSidPrincipals(env) {
|
|
131
|
+
const trusted = new Set();
|
|
132
|
+
const userSid = normalizeSid(env?.USERSID ?? "");
|
|
133
|
+
if (userSid && SID_RE.test(userSid) && !WORLD_SIDS.has(userSid))
|
|
134
|
+
trusted.add(userSid);
|
|
135
|
+
return trusted;
|
|
136
|
+
}
|
|
135
137
|
function buildTrustedPrincipals(env) {
|
|
136
|
-
const trusted = new Set(
|
|
138
|
+
const trusted = new Set([
|
|
139
|
+
...TRUSTED_BASE,
|
|
140
|
+
...buildTrustedSidPrincipals(env),
|
|
141
|
+
]);
|
|
137
142
|
const principal = resolveWindowsUserPrincipal(env);
|
|
138
143
|
if (principal) {
|
|
139
144
|
trusted.add(normalize(principal));
|
|
@@ -141,9 +146,6 @@ function buildTrustedPrincipals(env) {
|
|
|
141
146
|
if (userOnly)
|
|
142
147
|
trusted.add(normalize(userOnly));
|
|
143
148
|
}
|
|
144
|
-
const userSid = normalizeSid(env?.USERSID ?? "");
|
|
145
|
-
if (userSid && SID_RE.test(userSid) && !WORLD_SIDS.has(userSid))
|
|
146
|
-
trusted.add(userSid);
|
|
147
149
|
return trusted;
|
|
148
150
|
}
|
|
149
151
|
function classifyPrincipal(principal, trustedPrincipals) {
|
|
@@ -215,7 +217,11 @@ export function parseIcaclsOutput(output, targetPath) {
|
|
|
215
217
|
return entries;
|
|
216
218
|
}
|
|
217
219
|
export function summarizeWindowsAcl(entries, env) {
|
|
218
|
-
|
|
220
|
+
// Canonical SID entries cannot match account names. Classify them directly
|
|
221
|
+
// so structured descriptor facts do not depend on a separate user lookup.
|
|
222
|
+
const trustedPrincipals = entries.every((entry) => SID_RE.test(normalize(entry.sid ?? entry.principal)))
|
|
223
|
+
? buildTrustedSidPrincipals(env)
|
|
224
|
+
: buildTrustedPrincipals(env);
|
|
219
225
|
const trusted = [];
|
|
220
226
|
const untrustedWorld = [];
|
|
221
227
|
const untrustedGroup = [];
|
|
@@ -230,49 +236,75 @@ export function summarizeWindowsAcl(entries, env) {
|
|
|
230
236
|
}
|
|
231
237
|
return { trusted, untrustedWorld, untrustedGroup };
|
|
232
238
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (opts?.principalTranslationFailed)
|
|
243
|
-
throw new Error("Windows ACL principal SID translation failed");
|
|
244
|
-
const { stdout, stderr } = await exec(resolveWindowsSystemCommand("icacls.exe", opts?.env), [targetPath]);
|
|
245
|
-
let entries = parseIcaclsOutput(`${stdout}\n${stderr}`.trim(), targetPath);
|
|
246
|
-
if (!entries.length)
|
|
247
|
-
throw new Error("Windows ACL output could not be verified");
|
|
248
|
-
const unresolvedPrincipals = entries.filter((entry) => !entry.sid).map((entry) => entry.principal);
|
|
249
|
-
const principalSids = await resolveWindowsPrincipalSids({ principals: unresolvedPrincipals, known: opts?.principalSids, env: opts?.env, exec });
|
|
250
|
-
entries = entries.map((entry) => {
|
|
251
|
-
const sid = entry.sid ?? principalSids[entry.principal.toLowerCase()];
|
|
252
|
-
if (!sid)
|
|
253
|
-
throw new Error(`Windows ACL principal SID could not be verified: ${entry.principal}`);
|
|
254
|
-
return { ...entry, sid };
|
|
255
|
-
});
|
|
256
|
-
let currentUserSid = normalizeSid(opts?.currentUserSid ?? "");
|
|
257
|
-
let effectiveEnv = currentUserSid ? { USERSID: currentUserSid } : undefined;
|
|
258
|
-
let { trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv);
|
|
259
|
-
if (!currentUserSid && untrustedGroup.some((entry) => entry.sid && !TRUSTED_SIDS.has(entry.sid))) {
|
|
260
|
-
currentUserSid = (await resolveWindowsCurrentUserSid({ exec, env: opts?.env })) ?? "";
|
|
261
|
-
if (currentUserSid) {
|
|
262
|
-
effectiveEnv = { USERSID: currentUserSid };
|
|
263
|
-
({ trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return { ok: true, entries, trusted, untrustedWorld, untrustedGroup };
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
239
|
+
const MASK_RIGHTS = [
|
|
240
|
+
[0x1000_0000, "GA"], [0x8000_0000, "GR"], [0x4000_0000, "GW"], [0x2000_0000, "GE"],
|
|
241
|
+
[0x0001_0000, "D"], [0x0002_0000, "RC"], [0x0004_0000, "WDAC"], [0x0008_0000, "WO"],
|
|
242
|
+
[0x0010_0000, "S"], [0x0001, "RD"], [0x0002, "WD"], [0x0004, "AD"],
|
|
243
|
+
[0x0008, "REA"], [0x0010, "WEA"], [0x0020, "X"], [0x0040, "DC"], [0x0080, "RA"], [0x0100, "WA"],
|
|
244
|
+
];
|
|
245
|
+
function summarizeWindowsOwnerAcl(owner) {
|
|
246
|
+
const error = owner.error ?? owner.aclError;
|
|
247
|
+
if (error || owner.daclPresent === undefined || !owner.aces || !owner.currentUserSid) {
|
|
269
248
|
return {
|
|
270
249
|
ok: false, entries: [], trusted: [], untrustedWorld: [], untrustedGroup: [],
|
|
271
|
-
error:
|
|
272
|
-
errorDetail:
|
|
273
|
-
errorCause: err,
|
|
250
|
+
error: error ?? "Windows ACL query returned incomplete descriptor data",
|
|
251
|
+
errorDetail: owner.errorDetail, errorCause: owner.errorCause,
|
|
274
252
|
};
|
|
275
253
|
}
|
|
254
|
+
// A null DACL grants everyone full access; an empty DACL grants nothing.
|
|
255
|
+
const grants = owner.daclPresent ? owner.aces : [{ sid: "s-1-1-0", mask: 0x001f_01ff, deny: false, inheritOnly: false }];
|
|
256
|
+
const entries = grants.filter(ace => !ace.deny && !ace.inheritOnly).map(ace => {
|
|
257
|
+
const rights = ace.mask === 0x001f_01ff ? ["F"] : MASK_RIGHTS.filter(([mask]) => (ace.mask & mask) !== 0).map(([, name]) => name);
|
|
258
|
+
return {
|
|
259
|
+
principal: ace.sid, sid: ace.sid, rights, rawRights: "(" + rights.join(",") + ")",
|
|
260
|
+
// Retain the coarse policy's conservative treatment of read-control and
|
|
261
|
+
// mutation rights. Denies do not subtract grants or claim effective access.
|
|
262
|
+
canRead: (ace.mask & 0x9002_0089) !== 0,
|
|
263
|
+
canWrite: (ace.mask & 0x500d_0156) !== 0,
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
return { ok: true, entries, ...summarizeWindowsAcl(entries, { USERSID: owner.currentUserSid }) };
|
|
267
|
+
}
|
|
268
|
+
export async function inspectWindowsAcl(targetPath, opts) {
|
|
269
|
+
if (opts?.principalTranslationFailed) {
|
|
270
|
+
const error = new Error("Windows ACL principal SID translation failed");
|
|
271
|
+
return summarizeWindowsOwnerAcl({ error: String(error), errorCause: error });
|
|
272
|
+
}
|
|
273
|
+
let owner;
|
|
274
|
+
if (process.platform === "win32" && opts?.env === undefined && opts?.exec === undefined) {
|
|
275
|
+
try {
|
|
276
|
+
const native = getNativeBinding();
|
|
277
|
+
const stat = native && await safeStat(targetPath);
|
|
278
|
+
// Keep the named-query behavior for leaf links; the native reader follows them.
|
|
279
|
+
const facts = stat?.ok && !stat.isSymlink ? native?.readOwnerAndDacl(targetPath) : undefined;
|
|
280
|
+
if (facts && !facts.fallbackRequired && facts.isLocal && facts.aceListComplete && facts.unsupportedAceTypes.length === 0 &&
|
|
281
|
+
facts.aces.every(ace => ace.flags.inherited && ace.mask !== 0)) {
|
|
282
|
+
// .NET normalizes explicit ACEs, but retains nonzero inherited ACEs in
|
|
283
|
+
// their original order without merging them. Other forms keep its query.
|
|
284
|
+
// Adapt descriptor facts only: the public summary owns its SID and rights
|
|
285
|
+
// classification, which differs from the native secure-read policy.
|
|
286
|
+
owner = {
|
|
287
|
+
daclPresent: facts.daclPresent,
|
|
288
|
+
currentUserSid: facts.currentUserSid,
|
|
289
|
+
aces: facts.aces.map(ace => ({
|
|
290
|
+
sid: ace.sid,
|
|
291
|
+
mask: ace.mask,
|
|
292
|
+
deny: ace.aceType === "deny",
|
|
293
|
+
inheritOnly: ace.flags.inheritOnly,
|
|
294
|
+
})),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Preserve this inspector's structured fallback and command diagnostics
|
|
300
|
+
// when the optional helper or native descriptor query is unavailable.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
owner ??= await inspectWindowsOwner({ targetPath, env: opts?.env, exec: opts?.exec ?? defaultPermissionExec });
|
|
304
|
+
return summarizeWindowsOwnerAcl({
|
|
305
|
+
...owner,
|
|
306
|
+
currentUserSid: normalizeSid(opts?.currentUserSid ?? "") || owner.currentUserSid,
|
|
307
|
+
});
|
|
276
308
|
}
|
|
277
309
|
export function formatWindowsAclSummary(summary) {
|
|
278
310
|
if (!summary.ok)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAqDvB"}
|
package/dist/pinned-open.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isUnsafeDeviceReadPath } from "./device-path.js";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
6
7
|
function isExpectedPathError(error) {
|
|
7
8
|
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
8
9
|
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
@@ -22,7 +23,8 @@ export function openPinnedFileSync(params) {
|
|
|
22
23
|
return { ok: false, reason: "validation" };
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
const realPath = params.resolvedPath ??
|
|
26
|
+
const realPath = params.resolvedPath ??
|
|
27
|
+
(ioFs === fs ? realpathSync(params.filePath) : ioFs.realpathSync(params.filePath));
|
|
26
28
|
if (isUnsafeDeviceReadPath(realPath)) {
|
|
27
29
|
return { ok: false, reason: "validation" };
|
|
28
30
|
}
|
package/dist/pinned-operation.js
CHANGED
|
@@ -27,7 +27,7 @@ function validatePinnedRelativePath(relativePath) {
|
|
|
27
27
|
relativePath.startsWith("..\\")) {
|
|
28
28
|
throw new FsSafeError("invalid-path", "relative path must not escape root");
|
|
29
29
|
}
|
|
30
|
-
for (const segment of relativePath.split("/")) {
|
|
30
|
+
for (const segment of relativePath.split(process.platform === "win32" ? /[\\/]/ : "/")) {
|
|
31
31
|
if (segment === "..") {
|
|
32
32
|
throw new FsSafeError("invalid-path", "relative path must not contain '..'");
|
|
33
33
|
}
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import type { Readable } from "node:stream";
|
|
2
1
|
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
2
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
|
+
import { type CopyFileInput } from "./copy-file-input.js";
|
|
4
4
|
export type PinnedWriteInput = {
|
|
5
5
|
kind: "buffer";
|
|
6
6
|
data: string | Buffer;
|
|
7
7
|
encoding?: BufferEncoding;
|
|
8
8
|
} | {
|
|
9
9
|
kind: "stream";
|
|
10
|
-
stream:
|
|
11
|
-
|
|
10
|
+
stream: AsyncIterable<Uint8Array | string>;
|
|
11
|
+
stageBeforePublish?: boolean;
|
|
12
|
+
} | CopyFileInput;
|
|
12
13
|
export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
13
14
|
export type PublishedWriteIdentity = Readonly<{
|
|
14
15
|
dev: bigint;
|
|
@@ -22,10 +23,13 @@ export type PinnedWriteParams = {
|
|
|
22
23
|
mode: number;
|
|
23
24
|
sync?: boolean;
|
|
24
25
|
overwrite?: boolean;
|
|
26
|
+
assertBeforeMutation?: () => void;
|
|
27
|
+
rejectFinalSymlink?: boolean;
|
|
25
28
|
maxBytes?: number;
|
|
26
29
|
input: PinnedWriteInput;
|
|
27
30
|
rootIdentity?: FileIdentityStat;
|
|
28
31
|
onRenameIdentityMismatch?: "verify-content";
|
|
32
|
+
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
29
33
|
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
30
34
|
};
|
|
31
35
|
export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAOA,OAAO,EAA4F,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG7J,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAG7E,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5F,aAAa,CAAC;AA4ClB,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,gBAAgB,CAAC;IAC5C,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAEzD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -17,6 +17,10 @@ import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
|
17
17
|
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
18
18
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
19
19
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
20
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
21
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
22
|
+
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
23
|
+
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
20
24
|
function byteLength(input, encoding) {
|
|
21
25
|
return typeof input === "string"
|
|
22
26
|
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
@@ -36,20 +40,13 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
|
|
36
40
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
43
|
+
async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
|
|
40
44
|
let bytes = 0;
|
|
41
45
|
for await (const chunk of stream) {
|
|
42
46
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
43
47
|
bytes += buffer.byteLength;
|
|
44
48
|
assertWithinMaxBytes(bytes, maxBytes);
|
|
45
|
-
|
|
46
|
-
while (offset < buffer.byteLength) {
|
|
47
|
-
const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
|
|
48
|
-
if (bytesWritten <= 0) {
|
|
49
|
-
throw new FsSafeError("helper-failed", "fallback stream write made no progress");
|
|
50
|
-
}
|
|
51
|
-
offset += bytesWritten;
|
|
52
|
-
}
|
|
49
|
+
await writeAllToFile(handle, buffer, { assertBeforeMutation });
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
export async function runPinnedWriteHelper(params) {
|
|
@@ -95,7 +92,6 @@ async function runPinnedWriteFallback(params) {
|
|
|
95
92
|
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
96
93
|
if (exactRoot)
|
|
97
94
|
await inspectDirectoryIdentity(params.rootPath, exactRoot);
|
|
98
|
-
const guardOptions = { bigint: exactRoot !== undefined };
|
|
99
95
|
let parentPath = params.relativeParentPath
|
|
100
96
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
101
97
|
: params.rootPath;
|
|
@@ -107,15 +103,24 @@ async function runPinnedWriteFallback(params) {
|
|
|
107
103
|
parentPath = await mkdirPathComponentsWithGuards({
|
|
108
104
|
rootReal: params.rootPath,
|
|
109
105
|
targetPath: parentPath,
|
|
106
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
110
107
|
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
111
108
|
});
|
|
112
109
|
}
|
|
113
110
|
const parentGuard = params.mkdir
|
|
114
|
-
? await createAsyncDirectoryGuard(parentPath,
|
|
115
|
-
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath,
|
|
111
|
+
? await createAsyncDirectoryGuard(parentPath, { bigint: true })
|
|
112
|
+
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
|
|
116
113
|
const targetPath = path.join(parentPath, params.basename);
|
|
117
|
-
if (params.overwrite === false
|
|
118
|
-
|
|
114
|
+
if (params.overwrite === false && (params.input.kind === "buffer" ||
|
|
115
|
+
(params.input.kind === "stream" && !params.input.stageBeforePublish))) {
|
|
116
|
+
const assertBeforeMutation = () => {
|
|
117
|
+
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
118
|
+
params.assertBeforeMutation?.();
|
|
119
|
+
};
|
|
120
|
+
const handle = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
121
|
+
assertBeforeMutation();
|
|
122
|
+
return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode);
|
|
123
|
+
}, {
|
|
119
124
|
onPostGuardFailure: async (openedHandle) => {
|
|
120
125
|
// The parent failed verification, so targetPath may now resolve
|
|
121
126
|
// somewhere else. Close the fd, but do not clean up by path.
|
|
@@ -129,15 +134,12 @@ async function runPinnedWriteFallback(params) {
|
|
|
129
134
|
createdIdentity = verificationIdentity;
|
|
130
135
|
if (params.input.kind === "buffer") {
|
|
131
136
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
await handle.writeFile(params.input.data);
|
|
137
|
-
}
|
|
137
|
+
await writeAllToFile(handle, params.input.data, {
|
|
138
|
+
encoding: params.input.encoding, assertBeforeMutation,
|
|
139
|
+
});
|
|
138
140
|
}
|
|
139
141
|
else {
|
|
140
|
-
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
142
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes, assertBeforeMutation);
|
|
141
143
|
}
|
|
142
144
|
// Content writes may clear set-ID bits; finalize them through the owned fd.
|
|
143
145
|
await handle.chmod(params.mode);
|
|
@@ -176,20 +178,21 @@ async function runPinnedWriteFallback(params) {
|
|
|
176
178
|
let readHandle;
|
|
177
179
|
let renamed = false;
|
|
178
180
|
try {
|
|
181
|
+
params.assertBeforeMutation?.();
|
|
179
182
|
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
180
183
|
let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
181
184
|
tempIdentity = verificationIdentity;
|
|
182
185
|
if (params.input.kind === "buffer") {
|
|
183
186
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
await writeAllToFile(handle, params.input.data, {
|
|
188
|
+
encoding: params.input.encoding, assertBeforeMutation: params.assertBeforeMutation,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else if (params.input.kind === "file") {
|
|
192
|
+
await writeCopyFileToFd(handle.fd, params.input, params.maxBytes, params.assertBeforeMutation);
|
|
190
193
|
}
|
|
191
194
|
else {
|
|
192
|
-
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
195
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes, params.assertBeforeMutation);
|
|
193
196
|
}
|
|
194
197
|
tempStat = fsSync.fstatSync(handle.fd);
|
|
195
198
|
const tempPathStat = fsSync.lstatSync(tempPath);
|
|
@@ -200,10 +203,28 @@ async function runPinnedWriteFallback(params) {
|
|
|
200
203
|
await handle.chmod(params.mode);
|
|
201
204
|
if (params.sync !== false)
|
|
202
205
|
await syncFileBestEffort(handle);
|
|
206
|
+
if (params.input.kind === "file")
|
|
207
|
+
await params.input.verifySource();
|
|
203
208
|
let verifiedIdentity = expectedTempStat;
|
|
204
209
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
211
|
+
if (params.overwrite === false) {
|
|
212
|
+
publishCopyStage({
|
|
213
|
+
temporaryPath: tempPath, targetPath, fd: handle.fd,
|
|
214
|
+
identity: tempIdentity, parentGuard,
|
|
215
|
+
assertBeforeMutation: params.assertBeforeMutation,
|
|
216
|
+
onPublished: (identity) => {
|
|
217
|
+
renamed = true;
|
|
218
|
+
params.onPublished?.(identity);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
params.assertBeforeMutation?.();
|
|
224
|
+
await fs.rename(tempPath, targetPath);
|
|
225
|
+
renamed = true;
|
|
226
|
+
params.onPublished?.(verificationIdentity);
|
|
227
|
+
}
|
|
207
228
|
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
208
229
|
if (params.sync !== false)
|
|
209
230
|
await syncDirectoryBestEffort(parentPath);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
export type ReadFileWindowOptions = {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
};
|
|
5
|
+
/** Fills a caller-owned buffer from an explicit position, stopping at EOF. */
|
|
6
|
+
export declare function readFileWindowFully(handle: FileHandle, buffer: Buffer, position: number, { signal }?: ReadFileWindowOptions): Promise<number>;
|
|
7
|
+
/** Synchronous positional read; neither variant advances or closes its descriptor. */
|
|
8
|
+
export declare function readFileWindowFullySync(fd: number, buffer: Buffer, position: number): number;
|
|
9
|
+
//# sourceMappingURL=positional-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"positional-read.d.ts","sourceRoot":"","sources":["../src/positional-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AASF,8EAA8E;AAC9E,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,EAAE,MAAM,EAAE,GAAE,qBAA0B,GACrC,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAW5F"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
// Node's synchronous read length is a signed 32-bit value on supported runtimes.
|
|
3
|
+
const MAX_READ_BYTES = 0x7fffffff;
|
|
4
|
+
function validateWindow(buffer, position) {
|
|
5
|
+
if (!Number.isSafeInteger(position) || position < 0 ||
|
|
6
|
+
!Number.isSafeInteger(position + buffer.length)) {
|
|
7
|
+
throw new RangeError("read position and window end must be non-negative safe integers");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Fills a caller-owned buffer from an explicit position, stopping at EOF. */
|
|
11
|
+
export async function readFileWindowFully(handle, buffer, position, { signal } = {}) {
|
|
12
|
+
validateWindow(buffer, position);
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
let total = 0;
|
|
15
|
+
while (total < buffer.length) {
|
|
16
|
+
const { bytesRead } = await handle.read(buffer, total, Math.min(buffer.length - total, MAX_READ_BYTES), position + total);
|
|
17
|
+
// Settle the pending read before cancellation lets the caller reuse its buffer or handle.
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
if (bytesRead === 0)
|
|
20
|
+
break;
|
|
21
|
+
total += bytesRead;
|
|
22
|
+
}
|
|
23
|
+
return total;
|
|
24
|
+
}
|
|
25
|
+
/** Synchronous positional read; neither variant advances or closes its descriptor. */
|
|
26
|
+
export function readFileWindowFullySync(fd, buffer, position) {
|
|
27
|
+
validateWindow(buffer, position);
|
|
28
|
+
let total = 0;
|
|
29
|
+
while (total < buffer.length) {
|
|
30
|
+
const bytesRead = fs.readSync(fd, buffer, total, Math.min(buffer.length - total, MAX_READ_BYTES), position + total);
|
|
31
|
+
if (bytesRead === 0)
|
|
32
|
+
break;
|
|
33
|
+
total += bytesRead;
|
|
34
|
+
}
|
|
35
|
+
return total;
|
|
36
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAWzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAChC,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EACV,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,0BAA0B,CAAC;CAC5C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA4KF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAgGnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
|
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import { fileStore, fileStoreSync, } from "./file-store.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
7
|
import { isNotFoundPathError } from "./path.js";
|
|
8
|
+
import { realpathSync } from "./realpath.js";
|
|
9
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
8
10
|
import { throwFsSafeReadError } from "./root-errors.js";
|
|
9
11
|
import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
|
|
10
12
|
import { registerTempPathForExit, } from "./temp-cleanup.js";
|
|
@@ -66,7 +68,7 @@ function throwTempWorkspaceOpenFailure(failure) {
|
|
|
66
68
|
});
|
|
67
69
|
}
|
|
68
70
|
async function ensurePrivateDirectory(dir, mode) {
|
|
69
|
-
await fs.mkdir(dir, { recursive: true, mode });
|
|
71
|
+
await fs.mkdir(recursiveMkdirPath(dir), { recursive: true, mode });
|
|
70
72
|
const stat = fsSync.statSync(dir);
|
|
71
73
|
if (!stat.isDirectory()) {
|
|
72
74
|
throw new Error(`Temp root must be a directory: ${dir}`);
|
|
@@ -74,7 +76,7 @@ async function ensurePrivateDirectory(dir, mode) {
|
|
|
74
76
|
await fs.chmod(dir, mode).catch(() => undefined);
|
|
75
77
|
}
|
|
76
78
|
function ensurePrivateDirectorySync(dir, mode) {
|
|
77
|
-
fsSync.mkdirSync(dir, { recursive: true, mode });
|
|
79
|
+
fsSync.mkdirSync(recursiveMkdirPath(dir), { recursive: true, mode });
|
|
78
80
|
const stat = fsSync.statSync(dir);
|
|
79
81
|
if (!stat.isDirectory()) {
|
|
80
82
|
throw new Error(`Temp root must be a directory: ${dir}`);
|
|
@@ -93,7 +95,7 @@ async function createTempWorkspace(options) {
|
|
|
93
95
|
const requestedRoot = path.resolve(options.rootDir);
|
|
94
96
|
let root = requestedRoot;
|
|
95
97
|
try {
|
|
96
|
-
root =
|
|
98
|
+
root = realpathSync.native(requestedRoot);
|
|
97
99
|
}
|
|
98
100
|
catch {
|
|
99
101
|
root = requestedRoot;
|
|
@@ -194,7 +196,7 @@ export function tempWorkspaceSync(options) {
|
|
|
194
196
|
const requestedRoot = path.resolve(options.rootDir);
|
|
195
197
|
let root = requestedRoot;
|
|
196
198
|
try {
|
|
197
|
-
root =
|
|
199
|
+
root = realpathSync.native(requestedRoot);
|
|
198
200
|
}
|
|
199
201
|
catch {
|
|
200
202
|
root = requestedRoot;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
|
+
import type { PublishedWriteIdentity } from "./pinned-write.js";
|
|
4
|
+
export declare function publishCopyStage(params: {
|
|
5
|
+
temporaryPath: string;
|
|
6
|
+
targetPath: string;
|
|
7
|
+
fd: number;
|
|
8
|
+
identity: BigIntStats;
|
|
9
|
+
parentGuard: AnyAsyncDirectoryGuard;
|
|
10
|
+
assertBeforeMutation?: () => void;
|
|
11
|
+
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
12
|
+
}): void;
|
|
13
|
+
//# sourceMappingURL=publish-copy-stage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-copy-stage.d.ts","sourceRoot":"","sources":["../src/publish-copy-stage.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAIhE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;IACpC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC1D,GAAG,IAAI,CAqCP"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
|
+
// Node has no portable no-replace rename. Link only our completed private stage,
|
|
6
|
+
// then unlink it in the same JS turn so local readers never observe two names.
|
|
7
|
+
export function publishCopyStage(params) {
|
|
8
|
+
const { identity, temporaryPath, targetPath } = params;
|
|
9
|
+
const staged = fs.lstatSync(temporaryPath, { bigint: true });
|
|
10
|
+
const opened = fs.fstatSync(params.fd, { bigint: true });
|
|
11
|
+
if (!staged.isFile() || staged.isSymbolicLink() || staged.nlink !== 1n ||
|
|
12
|
+
!sameFileIdentityForCleanup(staged, identity) ||
|
|
13
|
+
!sameFileIdentityForCleanup(opened, identity)) {
|
|
14
|
+
throw new FsSafeError("path-mismatch", "exclusive copy stage changed before publication");
|
|
15
|
+
}
|
|
16
|
+
params.assertBeforeMutation?.();
|
|
17
|
+
fs.linkSync(temporaryPath, targetPath);
|
|
18
|
+
let observerRejected = false;
|
|
19
|
+
let observerError;
|
|
20
|
+
try {
|
|
21
|
+
params.onPublished?.(identity);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
observerRejected = true;
|
|
25
|
+
observerError = error;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
|
|
29
|
+
const current = fs.lstatSync(temporaryPath, { bigint: true });
|
|
30
|
+
if (parent.isSymbolicLink() || !parent.isDirectory() ||
|
|
31
|
+
!sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
|
|
32
|
+
current.isSymbolicLink() || !current.isFile() ||
|
|
33
|
+
!sameFileIdentityForCleanup(current, identity)) {
|
|
34
|
+
throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
|
|
35
|
+
}
|
|
36
|
+
// This removes the stage's name, never the published destination or source.
|
|
37
|
+
fs.unlinkSync(temporaryPath);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new FsSafeError("helper-failed", "published copy temporary cleanup failed", {
|
|
41
|
+
cause: observerRejected ? new AggregateError([observerError, error], "observer and cleanup failed") : error,
|
|
42
|
+
details: { publication: "published", path: targetPath, dev: identity.dev, ino: identity.ino, cleanup: "failed" },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (observerRejected)
|
|
46
|
+
throw observerError;
|
|
47
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;
|
|
1
|
+
{"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AA2ND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
|
package/dist/publish-file.js
CHANGED
|
@@ -90,9 +90,9 @@ async function copyPinnedSource(params) {
|
|
|
90
90
|
throw error;
|
|
91
91
|
}
|
|
92
92
|
let target;
|
|
93
|
-
let createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
|
|
94
|
-
rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
|
|
95
93
|
try {
|
|
94
|
+
let createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
|
|
95
|
+
rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
|
|
96
96
|
fsSync.fchmodSync(nativeFd, 0o600);
|
|
97
97
|
createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
|
|
98
98
|
await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
|
|
@@ -106,8 +106,9 @@ async function copyPinnedSource(params) {
|
|
|
106
106
|
throw new FsSafeError("path-mismatch", "native publication target changed after copy");
|
|
107
107
|
}
|
|
108
108
|
const hashed = await hashFileHandle(target, params.native);
|
|
109
|
-
|
|
109
|
+
const completedFd = nativeFd;
|
|
110
110
|
nativeFd = undefined;
|
|
111
|
+
fsSync.closeSync(completedFd);
|
|
111
112
|
return {
|
|
112
113
|
handle: target,
|
|
113
114
|
exactIdentity: opened,
|
|
@@ -126,9 +127,9 @@ async function copyPinnedSource(params) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
const target = await fs.open(params.targetPath, "wx+", 0o600);
|
|
129
|
-
const createdIdentity = fsSync.fstatSync(target.fd, { bigint: true });
|
|
130
|
-
rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
|
|
131
130
|
try {
|
|
131
|
+
const createdIdentity = fsSync.fstatSync(target.fd, { bigint: true });
|
|
132
|
+
rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
|
|
132
133
|
await target.chmod(0o600);
|
|
133
134
|
const exactIdentity = fsSync.fstatSync(target.fd, { bigint: true });
|
|
134
135
|
await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, exactIdentity);
|