@openclaw/fs-safe 0.13.1 → 0.15.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 +64 -1
- package/README.md +10 -4
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +113 -46
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +4 -4
- package/dist/archive-zip-directory.d.ts +4 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +2 -0
- package/dist/archive-zip-entry.d.ts +6 -2
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +23 -8
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +3 -4
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +107 -31
- package/dist/archive-zip-names.d.ts +1 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +6 -0
- package/dist/archive.js +6 -5
- package/dist/bounded-read-stream.d.ts +0 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +0 -6
- package/dist/copy-publication.d.ts +6 -0
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +3 -0
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +44 -24
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +29 -11
- package/dist/directory-mode-owner.js +5 -5
- package/dist/file-handle-transfer.d.ts +2 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +57 -2
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +18 -4
- package/dist/file-lock-sync-admission.d.ts +19 -0
- package/dist/file-lock-sync-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-admission.js +93 -0
- package/dist/file-lock-sync-root-acquire.d.ts +4 -0
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
- package/dist/file-lock-sync-root-acquire.js +370 -0
- package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
- package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-arbitration.js +66 -0
- package/dist/file-lock-sync-root-held.d.ts +34 -0
- package/dist/file-lock-sync-root-held.d.ts.map +1 -0
- package/dist/file-lock-sync-root-held.js +393 -0
- package/dist/file-lock-sync-root-io.d.ts +44 -0
- package/dist/file-lock-sync-root-io.d.ts.map +1 -0
- package/dist/file-lock-sync-root-io.js +209 -0
- package/dist/file-lock-sync-root-mutation.d.ts +17 -0
- package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
- package/dist/file-lock-sync-root-mutation.js +277 -0
- package/dist/file-lock-sync-root-options.d.ts +20 -0
- package/dist/file-lock-sync-root-options.d.ts.map +1 -0
- package/dist/file-lock-sync-root-options.js +58 -0
- package/dist/file-lock-sync-root-registration.d.ts +2 -0
- package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-registration.js +90 -0
- package/dist/file-lock-sync-root.d.ts +36 -0
- package/dist/file-lock-sync-root.d.ts.map +1 -0
- package/dist/file-lock-sync-root.js +361 -0
- package/dist/file-lock-sync-stale-admission.d.ts +24 -0
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-stale-admission.js +205 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +245 -205
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +5 -1
- package/dist/file-store-sync-directory.d.ts.map +1 -1
- package/dist/file-store-sync-directory.js +14 -7
- package/dist/file-store-sync-write.js +3 -3
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +47 -12
- package/dist/guest-dispatch-python.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +22 -15
- package/dist/json-durable-queue-ownership.d.ts +0 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +0 -6
- package/dist/native-binding.d.ts +8 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-parent-admission.d.ts +3 -2
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +24 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +7 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +202 -172
- package/dist/native-policy-directory-observation.d.ts +12 -0
- package/dist/native-policy-directory-observation.d.ts.map +1 -0
- package/dist/native-policy-directory-observation.js +61 -0
- package/dist/native-policy-parent-windows.d.ts +14 -0
- package/dist/native-policy-parent-windows.d.ts.map +1 -0
- package/dist/native-policy-parent-windows.js +200 -0
- package/dist/native-rename-outcome.d.ts +4 -0
- package/dist/native-rename-outcome.d.ts.map +1 -0
- package/dist/native-rename-outcome.js +8 -0
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +42 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +12 -8
- package/dist/path-prefix.d.ts.map +1 -1
- package/dist/path-prefix.js +30 -8
- package/dist/path-suffix-aliases.d.ts +2 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -1
- package/dist/path-suffix-aliases.js +25 -17
- package/dist/permission-exec.d.ts +2 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +150 -21
- package/dist/permissions-windows.js +1 -1
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +23 -29
- package/dist/pinned-mutation-observation.d.ts +7 -2
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +81 -18
- package/dist/pinned-mutation-shared-route.d.ts +1 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
- package/dist/pinned-mutation-shared-route.js +1 -1
- package/dist/pinned-write-types.d.ts +2 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +75 -121
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +1 -15
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +256 -18
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +62 -70
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -12
- package/dist/replace-file-temp-owner.d.ts +5 -2
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +65 -30
- package/dist/replace-file.js +6 -6
- package/dist/retained-directory-replacement.d.ts +26 -0
- package/dist/retained-directory-replacement.d.ts.map +1 -0
- package/dist/retained-directory-replacement.js +193 -0
- package/dist/root-boundary.d.ts +1 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +4 -0
- package/dist/root-context.d.ts +0 -8
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +0 -3
- package/dist/root-create-input.d.ts +6 -0
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +5 -1
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +5 -4
- package/dist/root-directory-list.d.ts +1 -0
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +20 -11
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +2 -2
- package/dist/root-path-errors.d.ts +1 -0
- package/dist/root-path-errors.d.ts.map +1 -1
- package/dist/root-path-errors.js +11 -2
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +12 -37
- package/dist/root-path.js +1 -13
- package/dist/root-remove.d.ts +1 -0
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +4 -0
- package/dist/root-walk.d.ts +1 -1
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +17 -2
- package/dist/root-write-admission.d.ts +0 -2
- package/dist/root-write-admission.d.ts.map +1 -1
- package/dist/root-write-admission.js +1 -15
- package/dist/root-write-compatibility.d.ts +1 -2
- package/dist/root-write-compatibility.d.ts.map +1 -1
- package/dist/root-write-compatibility.js +17 -68
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +10 -24
- package/dist/root-write-lock-binding.d.ts +15 -0
- package/dist/root-write-lock-binding.d.ts.map +1 -0
- package/dist/root-write-lock-binding.js +162 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +29 -42
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +3 -24
- package/dist/secret-read-policy.d.ts +6 -2
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +26 -2
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +23 -12
- package/dist/sidecar-lock-acquire.d.ts +2 -28
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +288 -199
- package/dist/sidecar-lock-admission-context.d.ts +19 -0
- package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-context.js +60 -0
- package/dist/sidecar-lock-admission-parser.d.ts +43 -0
- package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-parser.js +113 -0
- package/dist/sidecar-lock-admission.d.ts +35 -0
- package/dist/sidecar-lock-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-admission.js +7 -0
- package/dist/sidecar-lock-reclaim.d.ts +9 -4
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +80 -25
- package/dist/sidecar-lock-stale-admission.d.ts +39 -0
- package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-stale-admission.js +232 -0
- package/dist/sidecar-lock-target.d.ts +8 -0
- package/dist/sidecar-lock-target.d.ts.map +1 -0
- package/dist/sidecar-lock-target.js +55 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +100 -16
- package/dist/staged-directory.d.ts +5 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +37 -1
- package/dist/temp-workspace-admission.d.ts.map +1 -1
- package/dist/temp-workspace-admission.js +28 -4
- package/dist/temp-workspace-descriptor.d.ts.map +1 -1
- package/dist/temp-workspace-descriptor.js +9 -27
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +8 -8
- package/dist/walk.d.ts +5 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +19 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -2
- package/docs/advanced.md +1 -1
- package/docs/archive.md +36 -9
- package/docs/atomic.md +85 -8
- package/docs/copy.md +35 -0
- package/docs/file-store.md +19 -0
- package/docs/guest.md +1 -1
- package/docs/json-store.md +5 -0
- package/docs/native-helper.md +10 -3
- package/docs/native.md +8 -0
- package/docs/output.md +6 -0
- package/docs/path-prefix.md +10 -0
- package/docs/path-suffix-aliases.md +51 -6
- package/docs/permissions.md +13 -0
- package/docs/public-api.md +3 -2
- package/docs/root.md +22 -3
- package/docs/sidecar-lock.md +109 -4
- package/docs/staged-file.md +7 -3
- package/docs/temp.md +20 -3
- package/docs/walk.md +67 -1
- package/docs/writing.md +11 -1
- package/package.json +10 -10
- package/dist/darwin-acl.d.ts +0 -4
- package/dist/darwin-acl.d.ts.map +0 -1
- package/dist/darwin-acl.js +0 -24
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertDirectoryIdentitySync, inspectDirectoryIdentitySync, } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { openNativeParentAdmission } from "./native-parent-admission.js";
|
|
6
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
7
|
+
import { inspectNativeDirectoryObservation } from "./native-directory-observation.js";
|
|
8
|
+
import { createPathSegmentRoute, joinPathSegmentRoute } from "./path-segment-route.js";
|
|
9
|
+
import { isSymlinkOpenError } from "./path.js";
|
|
10
|
+
import { checkedMutationDirectory } from "./pinned-mutation-observation.js";
|
|
11
|
+
import { canReuseParentWithMutationAssertion } from "./root-write-lock-binding.js";
|
|
12
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
13
|
+
function assertParentCurrent(parent) {
|
|
14
|
+
const current = inspectFileIdentitySync(() => fsSync.fstatSync(parent.fd, { bigint: true }), parent.guard.stat);
|
|
15
|
+
assertDirectoryIdentitySync(parent.guard.dir, {
|
|
16
|
+
dev: parent.guard.stat.dev,
|
|
17
|
+
ino: parent.guard.stat.ino,
|
|
18
|
+
realPath: parent.guard.realPath,
|
|
19
|
+
});
|
|
20
|
+
return current;
|
|
21
|
+
}
|
|
22
|
+
function closeAfterFailure(closeFd, fd) {
|
|
23
|
+
if (fd === undefined)
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
closeFd(fd);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Windows retains the operation failure while attempting every owned close.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function capturePolicyAwareWindowsParent(binding, params, rootAdmission) {
|
|
33
|
+
const closeFd = captureNativeFdClose(binding);
|
|
34
|
+
let session;
|
|
35
|
+
function parentObservation(fd, guard) {
|
|
36
|
+
return checkedMutationDirectory(guard.dir, guard.realPath, guard.stat, typeof binding.observeDirectory === "function" ? () => {
|
|
37
|
+
const stat = inspectFileIdentitySync(() => fsSync.fstatSync(fd, { bigint: true }), guard.stat);
|
|
38
|
+
let observed;
|
|
39
|
+
try {
|
|
40
|
+
observed = inspectNativeDirectoryObservation(binding, guard.dir, stat);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error?.code === "OBSERVATION_UNAVAILABLE")
|
|
44
|
+
return undefined;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
return { canonicalPath: observed.realPath,
|
|
48
|
+
identity: { dev: stat.dev, ino: stat.ino, mode: stat.mode, nlink: stat.nlink } };
|
|
49
|
+
} : undefined);
|
|
50
|
+
}
|
|
51
|
+
async function openParent(fd, parentPath, relativePath) {
|
|
52
|
+
try {
|
|
53
|
+
const admitted = await openNativeParentAdmission(binding, {
|
|
54
|
+
...rootAdmission,
|
|
55
|
+
root: { fd },
|
|
56
|
+
rootPath: parentPath,
|
|
57
|
+
// Policy fences retain every identity bit even with a legacy numeric root.
|
|
58
|
+
exactRoot: true,
|
|
59
|
+
}, relativePath, "native-directory");
|
|
60
|
+
const guard = { ...admitted.guard, stat: admitted.guard.stat };
|
|
61
|
+
return { fd: admitted.fd, guard, observation: parentObservation(admitted.fd, guard) };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!isSymlinkOpenError(error))
|
|
65
|
+
throw error;
|
|
66
|
+
throw new FsSafeError(params.mutationAdmission.rejectParentSymlinks ? "symlink" : "path-mismatch", "native write parent changed during policy admission", { cause: error instanceof Error ? error : undefined });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function authorize(targetPath, mutationPath = targetPath, phase = "parent") {
|
|
70
|
+
const request = Object.freeze({ targetPath, mutationPath, phase });
|
|
71
|
+
return await (session ? session.authorize(request) : params.mutationAdmission.authorize(request));
|
|
72
|
+
}
|
|
73
|
+
// Existing complete parents need one open and one target admission. Exact
|
|
74
|
+
// parent denies apply only to creation, never to merely using a directory.
|
|
75
|
+
let complete;
|
|
76
|
+
try {
|
|
77
|
+
complete = await openParent(rootAdmission.root.fd, params.rootPath, params.relativeParentPath);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error?.code !== "ENOENT" || !params.mkdir)
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
if (complete) {
|
|
84
|
+
try {
|
|
85
|
+
await authorize(path.join(complete.guard.realPath, params.basename));
|
|
86
|
+
assertParentCurrent(complete);
|
|
87
|
+
return complete;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
closeAfterFailure(closeFd, complete.fd);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const segments = params.relativeParentPath.split("/").filter(Boolean);
|
|
95
|
+
const route = createPathSegmentRoute(segments);
|
|
96
|
+
const rootStat = inspectDirectoryIdentitySync(params.rootPath, inspectFileIdentitySync(() => fsSync.fstatSync(rootAdmission.root.fd, { bigint: true })));
|
|
97
|
+
let current = {
|
|
98
|
+
fd: rootAdmission.root.fd,
|
|
99
|
+
guard: { dir: params.rootPath, realPath: params.rootPath, stat: rootStat },
|
|
100
|
+
observation: parentObservation(rootAdmission.root.fd, {
|
|
101
|
+
dir: params.rootPath, realPath: params.rootPath, stat: rootStat,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
let ownedFd;
|
|
105
|
+
const initialTarget = joinPathSegmentRoute(params.rootPath, route, 0, params.basename);
|
|
106
|
+
if (canReuseParentWithMutationAssertion(params.assertBeforeMutation, params.rootPath, initialTarget)) {
|
|
107
|
+
session = params.mutationAdmission?.beginNativeParentWalk?.() ??
|
|
108
|
+
params.mutationAdmission?.beginSharedParentWalk?.();
|
|
109
|
+
if (session && session.retainedTargetPath !== initialTarget) {
|
|
110
|
+
session.dispose();
|
|
111
|
+
session = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
116
|
+
const segment = segments[index];
|
|
117
|
+
const parentPath = current.guard.realPath;
|
|
118
|
+
let child;
|
|
119
|
+
let createdByMkdir = false;
|
|
120
|
+
let createReceipt;
|
|
121
|
+
try {
|
|
122
|
+
child = await openParent(current.fd, parentPath, segment);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error?.code !== "ENOENT")
|
|
126
|
+
throw error;
|
|
127
|
+
// The request admits both the prospective leaf and this missing direct
|
|
128
|
+
// child. A deeper denial can leave already admitted ancestors intact.
|
|
129
|
+
const targetPath = session?.retainedTargetPath ??
|
|
130
|
+
joinPathSegmentRoute(parentPath, route, index, params.basename);
|
|
131
|
+
const mutationPath = path.join(parentPath, segment);
|
|
132
|
+
const request = Object.freeze({ targetPath, mutationPath, phase: "parent-create" });
|
|
133
|
+
createReceipt = session?.tryAuthorizeAtParent(request, current.observation);
|
|
134
|
+
if (!createReceipt) {
|
|
135
|
+
createReceipt = await authorize(targetPath, mutationPath, "parent-create");
|
|
136
|
+
assertParentCurrent(current);
|
|
137
|
+
}
|
|
138
|
+
if (params.assertBeforeMutation) {
|
|
139
|
+
// Opaque callbacks retain their post-callback descriptor/path fence.
|
|
140
|
+
params.assertBeforeMutation();
|
|
141
|
+
assertParentCurrent(current);
|
|
142
|
+
}
|
|
143
|
+
// Synchronous session admission already ends at the exact parent fence.
|
|
144
|
+
const mkdirChild = binding.mkdirChildBeneath;
|
|
145
|
+
if (typeof mkdirChild !== "function") {
|
|
146
|
+
throw new FsSafeError("helper-unavailable", "native direct-child parent creation is unavailable");
|
|
147
|
+
}
|
|
148
|
+
createdByMkdir = mkdirChild.call(binding, current.fd, segment, 0o777) === true;
|
|
149
|
+
child = await openParent(current.fd, parentPath, segment);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const targetPath = joinPathSegmentRoute(child.guard.realPath, route, index + 1, params.basename);
|
|
153
|
+
if (session && targetPath !== session.retainedTargetPath) {
|
|
154
|
+
session.dispose();
|
|
155
|
+
session = undefined;
|
|
156
|
+
}
|
|
157
|
+
let evidence;
|
|
158
|
+
if (session && createdByMkdir && createReceipt) {
|
|
159
|
+
try {
|
|
160
|
+
evidence = Object.freeze({
|
|
161
|
+
admission: createReceipt,
|
|
162
|
+
parent: parentObservation(current.fd, { ...current.guard,
|
|
163
|
+
stat: inspectFileIdentitySync(() => fsSync.fstatSync(current.fd, { bigint: true }), current.guard.stat),
|
|
164
|
+
}),
|
|
165
|
+
child: child.observation,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Incomplete optional evidence returns to ordered admission below.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Advancement refreshes both named directories after all Root/policy
|
|
173
|
+
// observations; these receipts supply expectations, not cached authority.
|
|
174
|
+
const advanced = evidence && session.advanceCreatedDirectory(evidence);
|
|
175
|
+
if (!advanced) {
|
|
176
|
+
await authorize(targetPath);
|
|
177
|
+
assertParentCurrent(child);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
closeAfterFailure(closeFd, child.fd);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const previousFd = ownedFd;
|
|
185
|
+
current = child;
|
|
186
|
+
ownedFd = child.fd;
|
|
187
|
+
if (previousFd !== undefined)
|
|
188
|
+
closeFd(previousFd);
|
|
189
|
+
}
|
|
190
|
+
if (ownedFd === undefined) {
|
|
191
|
+
throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
|
|
192
|
+
}
|
|
193
|
+
ownedFd = undefined;
|
|
194
|
+
return current;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
session?.dispose();
|
|
198
|
+
closeAfterFailure(closeFd, ownedFd);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
|
|
2
|
+
export type NativeRenameFailureOutcome = "uncommitted" | "indeterminate";
|
|
3
|
+
export declare function classifyNativeRenameFailure(error: unknown): NativeRenameFailureOutcome;
|
|
4
|
+
//# sourceMappingURL=native-rename-outcome.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-rename-outcome.d.ts","sourceRoot":"","sources":["../src/native-rename-outcome.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sCAAsC,qDACC,CAAC;AAErD,MAAM,MAAM,0BAA0B,GAAG,aAAa,GAAG,eAAe,CAAC;AAEzE,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAMtF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
|
|
2
|
+
export function classifyNativeRenameFailure(error) {
|
|
3
|
+
const code = error?.code ?? "";
|
|
4
|
+
// Ordinary errno can follow a committed remote rename whose reply was lost.
|
|
5
|
+
return code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH
|
|
6
|
+
? "uncommitted"
|
|
7
|
+
: "indeterminate";
|
|
8
|
+
}
|
|
@@ -8,8 +8,8 @@ export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding,
|
|
|
8
8
|
export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
|
|
9
9
|
declare class NativeStagedFile implements StagedFile {
|
|
10
10
|
#private;
|
|
11
|
-
constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
|
|
12
|
-
static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
|
|
11
|
+
constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void, name?: string);
|
|
12
|
+
static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void, exclusiveBasename?: string): Promise<NativeStagedFile>;
|
|
13
13
|
static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
14
14
|
get receipt(): StagedFileReceipt;
|
|
15
15
|
assertCurrent(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAG/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -62,6 +62,7 @@ import { writeNativeInput } from "./native-operations.js";
|
|
|
62
62
|
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
63
63
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
64
64
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
65
|
+
import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
65
66
|
export function assertNativeStaging(binding) {
|
|
66
67
|
if ([
|
|
67
68
|
binding.closeOwnedFd,
|
|
@@ -81,11 +82,6 @@ function assertBasename(name, portable) {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
const NOT_PUBLISHED = Object.freeze({ status: "not-published" });
|
|
84
|
-
const UNCOMMITTED_RENAME_ERRORS = new Set([
|
|
85
|
-
"EACCES", "EBADF", "EBUSY", "EEXIST", "EINVAL", "EISDIR", "ELOOP", "EMLINK",
|
|
86
|
-
"ENAMETOOLONG", "ENOENT", "ENOSPC", "ENOSYS", "ENOTDIR", "ENOTEMPTY", "ENOTSUP",
|
|
87
|
-
"EPERM", "EROFS", "ETXTBSY", "EXDEV",
|
|
88
|
-
]);
|
|
89
85
|
function failure(error, details) {
|
|
90
86
|
const code = error instanceof FsSafeError
|
|
91
87
|
? error.code
|
|
@@ -102,11 +98,13 @@ class NativeStagedFile {
|
|
|
102
98
|
#publishedMode;
|
|
103
99
|
#sync;
|
|
104
100
|
#assertBeforeMutation;
|
|
105
|
-
#name
|
|
101
|
+
#name;
|
|
106
102
|
#state = { status: "open", publication: NOT_PUBLISHED };
|
|
107
103
|
#receipt;
|
|
108
104
|
#rejectFinalSymlink = false;
|
|
109
|
-
constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
|
|
105
|
+
constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation, name = `.fs-safe-${randomUUID()}.tmp`) {
|
|
106
|
+
assertBasename(name, portableNames);
|
|
107
|
+
this.#name = name;
|
|
110
108
|
this.#closeFd = captureNativeFdClose(binding);
|
|
111
109
|
this.#binding = binding;
|
|
112
110
|
this.#parentFd = parentFd;
|
|
@@ -121,10 +119,10 @@ class NativeStagedFile {
|
|
|
121
119
|
// The supplied closer owns parentFd even on construction or preparation failure.
|
|
122
120
|
static async create(binding, parentFd, closeParentFd, directory, input, mode, maxBytes,
|
|
123
121
|
// Public staging uses portable names; existing POSIX writes accept literal names.
|
|
124
|
-
portableNames = true, sync = true, assertBeforeMutation) {
|
|
122
|
+
portableNames = true, sync = true, assertBeforeMutation, exclusiveBasename) {
|
|
125
123
|
let staged;
|
|
126
124
|
try {
|
|
127
|
-
staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation);
|
|
125
|
+
staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation, exclusiveBasename);
|
|
128
126
|
}
|
|
129
127
|
catch (error) {
|
|
130
128
|
try {
|
|
@@ -141,13 +139,20 @@ class NativeStagedFile {
|
|
|
141
139
|
static async write(binding, parentFd, closeParentFd, directory, params, parentGuard) {
|
|
142
140
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
143
141
|
try {
|
|
142
|
+
const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
|
|
144
143
|
// This owner never escapes. Only the internal verifier borrows its fd;
|
|
145
144
|
// public descriptor methods remain await-free and cannot race disposal.
|
|
146
|
-
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
|
|
145
|
+
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation, exclusive ? params.basename : undefined), true);
|
|
147
146
|
staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
|
|
148
147
|
if (params.input.kind === "file")
|
|
149
148
|
await params.input.verifySource();
|
|
150
|
-
|
|
149
|
+
if (exclusive) {
|
|
150
|
+
staged.#assertCurrent();
|
|
151
|
+
params.assertBeforeMutation?.();
|
|
152
|
+
}
|
|
153
|
+
const published = exclusive
|
|
154
|
+
? staged.#completePublication(params.basename, false, params.onPublished)
|
|
155
|
+
: await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
|
|
151
156
|
const identity = published.staged.identity;
|
|
152
157
|
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
153
158
|
return { dev: identity.dev, ino: identity.ino };
|
|
@@ -279,39 +284,13 @@ class NativeStagedFile {
|
|
|
279
284
|
}
|
|
280
285
|
}
|
|
281
286
|
catch (error) {
|
|
282
|
-
//
|
|
283
|
-
if (
|
|
287
|
+
// Only explicit pre-dispatch provenance can rule out a committed rename.
|
|
288
|
+
if (classifyNativeRenameFailure(error) === "indeterminate") {
|
|
284
289
|
state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
|
|
285
290
|
}
|
|
286
291
|
throw error;
|
|
287
292
|
}
|
|
288
|
-
|
|
289
|
-
const receipt = Object.freeze({
|
|
290
|
-
status: "published",
|
|
291
|
-
staged: this.receipt,
|
|
292
|
-
basename,
|
|
293
|
-
overwrite,
|
|
294
|
-
});
|
|
295
|
-
state.publication = receipt;
|
|
296
|
-
onPublished?.(receipt.staged.identity);
|
|
297
|
-
const stagedMode = this.#assertNamed(basename);
|
|
298
|
-
assertStagedDirectoryCurrent(this.#directory);
|
|
299
|
-
// Keep contents private until the published name passes its identity
|
|
300
|
-
// fence. Mode changes use the owned fd, including for final mode 000.
|
|
301
|
-
const fd = this.#file();
|
|
302
|
-
if (this.#publishedMode !== 0o600 || stagedMode !== 0o600)
|
|
303
|
-
fs.fchmodSync(fd, this.#publishedMode);
|
|
304
|
-
// With sync enabled, content was synced before rename. A lost chmod leaves staged 0600,
|
|
305
|
-
// no wider than modes retaining owner rw. Restrictive modes and observed
|
|
306
|
-
// widening of the stage still need the permission correction made durable.
|
|
307
|
-
if (this.#sync && ((this.#publishedMode & 0o600) !== 0o600 || (stagedMode & ~this.#publishedMode) !== 0)) {
|
|
308
|
-
syncFileBestEffortSync(fd);
|
|
309
|
-
}
|
|
310
|
-
if (this.#sync)
|
|
311
|
-
syncFileBestEffortSync(this.#parentFd);
|
|
312
|
-
this.#assertNamed(basename);
|
|
313
|
-
assertStagedDirectoryCurrent(this.#directory);
|
|
314
|
-
return receipt;
|
|
293
|
+
return this.#completePublication(basename, overwrite, onPublished);
|
|
315
294
|
}
|
|
316
295
|
catch (error) {
|
|
317
296
|
if (error instanceof MutationAuthorityError)
|
|
@@ -323,6 +302,29 @@ class NativeStagedFile {
|
|
|
323
302
|
throw failure(error, { phase: "publish", publication });
|
|
324
303
|
}
|
|
325
304
|
}
|
|
305
|
+
#completePublication(basename, overwrite, onPublished) {
|
|
306
|
+
// Record complete content before fallible post-publication verification.
|
|
307
|
+
const receipt = Object.freeze({ status: "published", staged: this.receipt, basename, overwrite });
|
|
308
|
+
this.#open().publication = receipt;
|
|
309
|
+
onPublished?.(receipt.staged.identity);
|
|
310
|
+
const stagedMode = this.#assertNamed(basename);
|
|
311
|
+
assertStagedDirectoryCurrent(this.#directory);
|
|
312
|
+
// Keep contents private until the name passes its identity fence. Mode
|
|
313
|
+
// changes use the owned fd, including for final mode 000.
|
|
314
|
+
const fd = this.#file();
|
|
315
|
+
if (this.#publishedMode !== 0o600 || stagedMode !== 0o600)
|
|
316
|
+
fs.fchmodSync(fd, this.#publishedMode);
|
|
317
|
+
// Content was synced during preparation. A lost chmod leaves 0600, no wider
|
|
318
|
+
// than modes retaining owner rw; restrictive modes still need a durable correction.
|
|
319
|
+
if (this.#sync && ((this.#publishedMode & 0o600) !== 0o600 || (stagedMode & ~this.#publishedMode) !== 0)) {
|
|
320
|
+
syncFileBestEffortSync(fd);
|
|
321
|
+
}
|
|
322
|
+
if (this.#sync)
|
|
323
|
+
syncFileBestEffortSync(this.#parentFd);
|
|
324
|
+
this.#assertNamed(basename);
|
|
325
|
+
assertStagedDirectoryCurrent(this.#directory);
|
|
326
|
+
return receipt;
|
|
327
|
+
}
|
|
326
328
|
#finalize() {
|
|
327
329
|
if (this.#state.status === "closed") {
|
|
328
330
|
return this.#state;
|
package/dist/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA+DrC"}
|
package/dist/output.js
CHANGED
|
@@ -57,6 +57,10 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
57
57
|
const requestedTargetPath = options.path;
|
|
58
58
|
assertNoWindowsPathAlias(path.dirname(requestedTargetPath), "filesystem", "output target parent uses a Windows filesystem namespace alias");
|
|
59
59
|
const producerIsolation = options.producerIsolation;
|
|
60
|
+
const write = options.write;
|
|
61
|
+
const mode = options.mode;
|
|
62
|
+
const staging = options.staging;
|
|
63
|
+
const fallbackFileName = options.fallbackFileName;
|
|
60
64
|
const targetRoot = await root(rootDir);
|
|
61
65
|
if (requestedTargetPath.length === 0) {
|
|
62
66
|
throw new FsSafeError("invalid-path", "target path is required");
|
|
@@ -68,11 +72,11 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
68
72
|
targetPath: requestedTargetPath,
|
|
69
73
|
});
|
|
70
74
|
assertFileTargetPath(rawTargetPath);
|
|
71
|
-
const targetPath = sanitizedTargetPath(rawTargetPath,
|
|
75
|
+
const targetPath = sanitizedTargetPath(rawTargetPath, fallbackFileName);
|
|
72
76
|
assertNoWindowsPathAlias(targetPath, "filesystem", "output target uses a Windows filesystem namespace alias");
|
|
73
77
|
const finalPath = await targetRoot.resolve(targetPath);
|
|
74
78
|
assertNoWindowsPathAlias(finalPath, "filesystem", "output target uses a Windows filesystem namespace alias");
|
|
75
|
-
if (
|
|
79
|
+
if (staging === "sibling") {
|
|
76
80
|
const parentPath = path.dirname(targetPath);
|
|
77
81
|
if (parentPath !== ".") {
|
|
78
82
|
await targetRoot.mkdir(parentPath);
|
|
@@ -80,23 +84,23 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
80
84
|
const siblingFinalPath = await targetRoot.resolve(targetPath);
|
|
81
85
|
const result = await writeExternalFileViaSibling({
|
|
82
86
|
finalPath: siblingFinalPath,
|
|
83
|
-
write
|
|
87
|
+
write,
|
|
84
88
|
producerIsolation,
|
|
85
|
-
fallbackFileName
|
|
89
|
+
fallbackFileName,
|
|
86
90
|
maxBytes,
|
|
87
|
-
mode
|
|
91
|
+
mode,
|
|
88
92
|
});
|
|
89
93
|
return { path: siblingFinalPath, result };
|
|
90
94
|
}
|
|
91
95
|
const staged = await tempFile({
|
|
92
96
|
prefix: "fs-safe-output",
|
|
93
|
-
fileName: tempFileNameForTarget(targetPath,
|
|
97
|
+
fileName: tempFileNameForTarget(targetPath, fallbackFileName),
|
|
94
98
|
});
|
|
95
99
|
try {
|
|
96
|
-
const result = await
|
|
100
|
+
const result = await Function.prototype.call.call(write, options, staged.path);
|
|
97
101
|
await targetRoot.copyIn(targetPath, staged.path, {
|
|
98
102
|
maxBytes,
|
|
99
|
-
mode
|
|
103
|
+
mode,
|
|
100
104
|
mkdir: true,
|
|
101
105
|
sourceHardlinks: "reject",
|
|
102
106
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-prefix.d.ts","sourceRoot":"","sources":["../src/path-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;
|
|
1
|
+
{"version":3,"file":"path-prefix.d.ts","sourceRoot":"","sources":["../src/path-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;AAuBF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAuEvE"}
|
package/dist/path-prefix.js
CHANGED
|
@@ -19,15 +19,23 @@ function resolutionError(code, absolutePath) {
|
|
|
19
19
|
code, syscall: "resolvePathPrefix", path: absolutePath,
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
// Leave ample room for ordinary shallow paths to retain the legacy small-array
|
|
23
|
+
// path. At 32 components, repeated front removal can reindex at most 496 array
|
|
24
|
+
// positions per constructed queue; longer queues use a forward cursor instead.
|
|
25
|
+
const SHIFT_QUEUE_COMPONENT_LIMIT = 32;
|
|
22
26
|
export function resolvePathPrefixSync(input) {
|
|
23
27
|
assertNoNulPathInput(input);
|
|
24
28
|
const absolutePath = absolutePathWithRawSegments(input);
|
|
25
29
|
let resolved = rawRoot(absolutePath);
|
|
26
|
-
|
|
30
|
+
let remaining = absolutePath.slice(resolved.length).split(path.sep);
|
|
31
|
+
let shiftQueue = remaining.length <= SHIFT_QUEUE_COMPONENT_LIMIT;
|
|
32
|
+
let nextSegment = 0;
|
|
33
|
+
let remainingCount = remaining.length;
|
|
27
34
|
const visitedStates = new Set();
|
|
28
35
|
let symlinkHops = 0;
|
|
29
|
-
while (
|
|
30
|
-
const segment = remaining.shift();
|
|
36
|
+
while (remainingCount > 0) {
|
|
37
|
+
const segment = shiftQueue ? remaining.shift() : remaining[nextSegment++];
|
|
38
|
+
remainingCount--;
|
|
31
39
|
if (segment === "")
|
|
32
40
|
continue;
|
|
33
41
|
if (segment === "." || segment === "..") {
|
|
@@ -51,32 +59,46 @@ export function resolvePathPrefixSync(input) {
|
|
|
51
59
|
return {
|
|
52
60
|
absolutePath,
|
|
53
61
|
existingPath: realpathSync.native(pathForWindowsFilesystem(resolved)),
|
|
54
|
-
unresolvedSegments: [segment, ...remaining],
|
|
62
|
+
unresolvedSegments: shiftQueue ? [segment, ...remaining] : remaining.slice(nextSegment - 1),
|
|
55
63
|
};
|
|
56
64
|
}
|
|
57
65
|
if (!stat.isSymbolicLink()) {
|
|
58
|
-
if (!stat.isDirectory() &&
|
|
66
|
+
if (!stat.isDirectory() && remainingCount > 0)
|
|
59
67
|
throw resolutionError("ENOTDIR", absolutePath);
|
|
60
68
|
resolved = candidate;
|
|
61
69
|
continue;
|
|
62
70
|
}
|
|
63
|
-
const
|
|
71
|
+
const pendingSuffix = shiftQueue ? remaining : remaining.slice(nextSegment);
|
|
72
|
+
const state = JSON.stringify([String(stat.dev), String(stat.ino), candidate, pendingSuffix]);
|
|
64
73
|
if (symlinkHops >= 64 || visitedStates.has(state))
|
|
65
74
|
throw resolutionError("ELOOP", absolutePath);
|
|
66
75
|
visitedStates.add(state);
|
|
67
76
|
symlinkHops++;
|
|
68
77
|
const target = fs.readlinkSync(candidate);
|
|
69
78
|
const rawTarget = path.sep === "\\" ? target.replaceAll("/", "\\") : target;
|
|
79
|
+
let targetSegments;
|
|
70
80
|
if (path.isAbsolute(rawTarget)) {
|
|
71
81
|
const targetRoot = rawRoot(rawTarget);
|
|
72
82
|
// A rooted Windows link target uses the link's drive/share, not cwd's.
|
|
73
83
|
resolved = path.sep === "\\" && targetRoot === "\\" ? rawRoot(resolved) : targetRoot;
|
|
74
|
-
|
|
84
|
+
targetSegments = rawTarget.slice(targetRoot.length).split(path.sep);
|
|
75
85
|
}
|
|
76
86
|
else {
|
|
77
87
|
// A target's parent traversal applies after resolving its preceding links.
|
|
78
|
-
|
|
88
|
+
targetSegments = rawTarget.split(path.sep);
|
|
79
89
|
}
|
|
90
|
+
const expandedCount = targetSegments.length + pendingSuffix.length;
|
|
91
|
+
if (expandedCount <= SHIFT_QUEUE_COMPONENT_LIMIT) {
|
|
92
|
+
// The spread is bounded by the same fixed limit as subsequent shifts.
|
|
93
|
+
remaining = pendingSuffix;
|
|
94
|
+
remaining.unshift(...targetSegments);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
remaining = targetSegments.concat(pendingSuffix);
|
|
98
|
+
}
|
|
99
|
+
remainingCount = remaining.length;
|
|
100
|
+
shiftQueue = remainingCount <= SHIFT_QUEUE_COMPONENT_LIMIT;
|
|
101
|
+
nextSegment = 0;
|
|
80
102
|
}
|
|
81
103
|
return { absolutePath, existingPath: realpathSync.native(pathForWindowsFilesystem(resolved)), unresolvedSegments: [] };
|
|
82
104
|
}
|
|
@@ -2,6 +2,8 @@ export type ProbePathSuffixAliasesOptions = {
|
|
|
2
2
|
directory: string;
|
|
3
3
|
left: string;
|
|
4
4
|
right: string;
|
|
5
|
+
/** Maximum relative component count; defaults to 32. */
|
|
6
|
+
maxDepth?: number;
|
|
5
7
|
/** Synchronous caller policy for differing NFC pairs not equivalent under ASCII case folding. */
|
|
6
8
|
shouldProbeCaseVariants?: (leftNfc: string, rightNfc: string) => boolean;
|
|
7
9
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-suffix-aliases.d.ts","sourceRoot":"","sources":["../src/path-suffix-aliases.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1E,CAAC;
|
|
1
|
+
{"version":3,"file":"path-suffix-aliases.d.ts","sourceRoot":"","sources":["../src/path-suffix-aliases.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1E,CAAC;AAkVF,4FAA4F;AAC5F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,GAAG,SAAS,CAmDtG"}
|