@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
|
@@ -15,6 +15,7 @@ const MAX_SUFFIX_DEPTH = 32;
|
|
|
15
15
|
const MAX_MKDIR_ATTEMPTS = 128;
|
|
16
16
|
const MAX_CREATED_DIRECTORIES = 64;
|
|
17
17
|
const MAX_FORWARD_OBSERVATIONS = 4_096;
|
|
18
|
+
const MAX_SCALED_FORWARD_OBSERVATIONS = 32_768;
|
|
18
19
|
const PROBE_NAME_LENGTH = 6;
|
|
19
20
|
const PROBE_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
20
21
|
const PROBE_FIRST_ALPHABET = "bdefghijkmoqrstuvwxyz";
|
|
@@ -25,7 +26,7 @@ function assertMutationPath(value) {
|
|
|
25
26
|
if (value.length > MAX_PATH_LENGTH)
|
|
26
27
|
unavailable();
|
|
27
28
|
}
|
|
28
|
-
function suffixSegments(value) {
|
|
29
|
+
function suffixSegments(value, maxDepth) {
|
|
29
30
|
if (typeof value !== "string")
|
|
30
31
|
throw new TypeError("suffix must be a string");
|
|
31
32
|
if (value.length > MAX_SUFFIX_LENGTH)
|
|
@@ -35,8 +36,8 @@ function suffixSegments(value) {
|
|
|
35
36
|
}
|
|
36
37
|
const windows = process.platform === "win32";
|
|
37
38
|
const segments = (windows ? value.replaceAll("/", "\\") : value).split(path.sep);
|
|
38
|
-
if (segments.length >
|
|
39
|
-
throw new RangeError(
|
|
39
|
+
if (segments.length > maxDepth)
|
|
40
|
+
throw new RangeError(`suffix exceeds ${maxDepth} components`);
|
|
40
41
|
if (segments.some(segment => !segment || segment === "." || segment === ".." ||
|
|
41
42
|
(windows && (segment.includes(":") || /^[ .]+$/u.test(segment) ||
|
|
42
43
|
isWindowsReservedDeviceName(segment))))) {
|
|
@@ -96,14 +97,8 @@ function createPrivateProbeName(nameLength) {
|
|
|
96
97
|
return name;
|
|
97
98
|
}
|
|
98
99
|
function* asciiCaseProbePairs(nameLength, forbidden) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const first = createPrivateProbeName(nameLength);
|
|
102
|
-
const second = `${first[0].toUpperCase()}${first.slice(1)}`;
|
|
103
|
-
if (!forbidden(first) && !forbidden(second)) {
|
|
104
|
-
count++;
|
|
105
|
-
yield [first, second];
|
|
106
|
-
}
|
|
100
|
+
for (const first of privateProbeNames(nameLength, forbidden)) {
|
|
101
|
+
yield [first, `${first[0].toUpperCase()}${first.slice(1)}`];
|
|
107
102
|
}
|
|
108
103
|
}
|
|
109
104
|
function* privateProbeNames(nameLength, forbidden) {
|
|
@@ -128,8 +123,16 @@ class ProbeDirectories {
|
|
|
128
123
|
observations = 0;
|
|
129
124
|
mkdirAttempts = 0;
|
|
130
125
|
creations = 0;
|
|
131
|
-
|
|
126
|
+
maxObservations;
|
|
127
|
+
maxMkdirAttempts;
|
|
128
|
+
maxCreations;
|
|
129
|
+
constructor(requestedDirectory, depth) {
|
|
132
130
|
this.requestedDirectory = requestedDirectory;
|
|
131
|
+
// The suffix-length limit bounds actual depth, so these products remain safe integers.
|
|
132
|
+
const scale = Math.max(1, depth / MAX_SUFFIX_DEPTH);
|
|
133
|
+
this.maxObservations = Math.min(MAX_SCALED_FORWARD_OBSERVATIONS, MAX_FORWARD_OBSERVATIONS * scale * scale);
|
|
134
|
+
this.maxMkdirAttempts = MAX_MKDIR_ATTEMPTS * scale;
|
|
135
|
+
this.maxCreations = MAX_CREATED_DIRECTORIES * scale;
|
|
133
136
|
this.observe(1);
|
|
134
137
|
const canonical = realpathSync.native(pathForWindowsFilesystem(requestedDirectory));
|
|
135
138
|
assertMutationPath(canonical);
|
|
@@ -138,7 +141,7 @@ class ProbeDirectories {
|
|
|
138
141
|
this.assert(this.root);
|
|
139
142
|
}
|
|
140
143
|
reserveObservations(units) {
|
|
141
|
-
if (this.
|
|
144
|
+
if (units > this.maxObservations - this.observations)
|
|
142
145
|
unavailable();
|
|
143
146
|
}
|
|
144
147
|
observe(units, cleanup = false) {
|
|
@@ -159,7 +162,7 @@ class ProbeDirectories {
|
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
create(parent, name) {
|
|
162
|
-
if (this.mkdirAttempts >=
|
|
165
|
+
if (this.mkdirAttempts >= this.maxMkdirAttempts || this.creations >= this.maxCreations)
|
|
163
166
|
unavailable();
|
|
164
167
|
const probePath = path.join(parent.path, name);
|
|
165
168
|
assertMutationPath(probePath);
|
|
@@ -337,11 +340,16 @@ export function probePathSuffixAliasesSync(options) {
|
|
|
337
340
|
const directory = resolvePathPreservingWindowsRoot(requestedDirectory);
|
|
338
341
|
if (directory.length > MAX_PATH_LENGTH)
|
|
339
342
|
throw new RangeError("resolved directory exceeds 32768 code units");
|
|
343
|
+
const requestedMaxDepth = options.maxDepth;
|
|
344
|
+
const maxDepth = requestedMaxDepth === undefined ? MAX_SUFFIX_DEPTH : requestedMaxDepth;
|
|
345
|
+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) {
|
|
346
|
+
throw new RangeError("maxDepth must be a non-negative safe integer");
|
|
347
|
+
}
|
|
340
348
|
const leftInput = options.left;
|
|
341
349
|
const rightInput = options.right;
|
|
342
350
|
const predicate = options.shouldProbeCaseVariants;
|
|
343
|
-
const left = suffixSegments(leftInput);
|
|
344
|
-
const right = suffixSegments(rightInput);
|
|
351
|
+
const left = suffixSegments(leftInput, maxDepth);
|
|
352
|
+
const right = suffixSegments(rightInput, maxDepth);
|
|
345
353
|
if (left.length !== right.length)
|
|
346
354
|
throw new TypeError("suffixes must have the same component count");
|
|
347
355
|
if (predicate !== undefined && typeof predicate !== "function") {
|
|
@@ -370,7 +378,7 @@ export function probePathSuffixAliasesSync(options) {
|
|
|
370
378
|
let observed;
|
|
371
379
|
let cleaned = true;
|
|
372
380
|
try {
|
|
373
|
-
observed = observeSuffixes(() => owner ??= new ProbeDirectories(directory), left, right, shouldProbe);
|
|
381
|
+
observed = observeSuffixes(() => owner ??= new ProbeDirectories(directory, left.length), left, right, shouldProbe);
|
|
374
382
|
owner?.assert(owner.root);
|
|
375
383
|
}
|
|
376
384
|
catch {
|
|
@@ -8,6 +8,8 @@ export type PermissionCommandFailure = {
|
|
|
8
8
|
stderr: string;
|
|
9
9
|
};
|
|
10
10
|
export declare function formatPermissionErrorDetail(value: string): string;
|
|
11
|
+
/** Formats only caught permission-query failures without invoking user code. */
|
|
12
|
+
export declare function formatCaughtPermissionFailure(error: unknown): string;
|
|
11
13
|
export declare class PermissionCommandError extends Error implements PermissionCommandFailure {
|
|
12
14
|
readonly command: string;
|
|
13
15
|
readonly durationMs: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission-exec.d.ts","sourceRoot":"","sources":["../src/permission-exec.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjE;
|
|
1
|
+
{"version":3,"file":"permission-exec.d.ts","sourceRoot":"","sources":["../src/permission-exec.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjE;AA4ED,gFAAgF;AAChF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAqBpE;AA2CD,qBAAa,sBAAuB,SAAQ,KAAM,YAAW,wBAAwB;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YACE,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,OAAO,EACd,SAAS,SAAqC,EAc/C;CACF;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,wBAAwB,GAAG,SAAS,CAgCtC;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,SAAqC,GAC7C,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAa7C"}
|
package/dist/permission-exec.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { promisify } from "node:util";
|
|
3
|
+
import { promisify, types as utilTypes } from "node:util";
|
|
4
4
|
import { formatErrorDetail } from "./error-detail.js";
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
6
|
export const DEFAULT_PERMISSION_EXEC_TIMEOUT_MS = 30_000;
|
|
@@ -8,15 +8,127 @@ export function formatPermissionErrorDetail(value) {
|
|
|
8
8
|
const formatted = formatErrorDetail(value);
|
|
9
9
|
return formatted.length > 400 ? `${formatted.slice(0, 399)}…` : formatted;
|
|
10
10
|
}
|
|
11
|
+
const MISSING_DATA_PROPERTY = Object.freeze({ found: false });
|
|
12
|
+
const MAX_CAUGHT_FAILURE_PROTOTYPES = 4;
|
|
13
|
+
const primitiveString = String;
|
|
14
|
+
const isUint8Array = utilTypes.isUint8Array;
|
|
15
|
+
const Uint8ArrayIntrinsic = Uint8Array;
|
|
16
|
+
const typedArrayPrototype = Object.getPrototypeOf(Uint8ArrayIntrinsic.prototype);
|
|
17
|
+
const typedArrayBufferGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
|
|
18
|
+
const typedArrayByteOffsetGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteOffset").get;
|
|
19
|
+
const typedArrayByteLengthGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
|
|
20
|
+
const typedArraySet = Object.getOwnPropertyDescriptor(typedArrayPrototype, "set").value;
|
|
21
|
+
const textDecoderDecode = TextDecoder.prototype.decode;
|
|
22
|
+
const stderrDecoder = new TextDecoder("utf-8", {
|
|
23
|
+
fatal: false,
|
|
24
|
+
ignoreBOM: true,
|
|
25
|
+
});
|
|
26
|
+
const MAX_STDERR_BYTES = 1600;
|
|
27
|
+
function isInspectableCaughtObject(value) {
|
|
28
|
+
if ((typeof value !== "object" || value === null) && typeof value !== "function") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return !utilTypes.isProxy(value);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function dataProperty(value, name) {
|
|
39
|
+
if (!isInspectableCaughtObject(value))
|
|
40
|
+
return MISSING_DATA_PROPERTY;
|
|
41
|
+
let current = value;
|
|
42
|
+
for (let depth = 0; current !== null && depth < MAX_CAUGHT_FAILURE_PROTOTYPES; depth += 1) {
|
|
43
|
+
if (!isInspectableCaughtObject(current))
|
|
44
|
+
return MISSING_DATA_PROPERTY;
|
|
45
|
+
let descriptor;
|
|
46
|
+
try {
|
|
47
|
+
descriptor = Object.getOwnPropertyDescriptor(current, name);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return MISSING_DATA_PROPERTY;
|
|
51
|
+
}
|
|
52
|
+
if (descriptor) {
|
|
53
|
+
return Object.hasOwn(descriptor, "value")
|
|
54
|
+
? { found: true, value: descriptor.value }
|
|
55
|
+
: MISSING_DATA_PROPERTY;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
current = Object.getPrototypeOf(current);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return MISSING_DATA_PROPERTY;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return MISSING_DATA_PROPERTY;
|
|
65
|
+
}
|
|
66
|
+
function caughtPrimitiveDisplay(value) {
|
|
67
|
+
if (value !== null && (typeof value === "object" || typeof value === "function"))
|
|
68
|
+
return undefined;
|
|
69
|
+
return primitiveString(value);
|
|
70
|
+
}
|
|
71
|
+
/** Formats only caught permission-query failures without invoking user code. */
|
|
72
|
+
export function formatCaughtPermissionFailure(error) {
|
|
73
|
+
const primitive = caughtPrimitiveDisplay(error);
|
|
74
|
+
if (primitive !== undefined)
|
|
75
|
+
return formatBoundedCaughtDetail(primitive);
|
|
76
|
+
if (!isInspectableCaughtObject(error)) {
|
|
77
|
+
return "Uninspectable proxy failure";
|
|
78
|
+
}
|
|
79
|
+
const message = dataProperty(error, "message");
|
|
80
|
+
const name = dataProperty(error, "name");
|
|
81
|
+
const rawMessage = message.found && typeof message.value === "string"
|
|
82
|
+
? message.value : "";
|
|
83
|
+
const rawName = name.found && typeof name.value === "string"
|
|
84
|
+
? name.value : "";
|
|
85
|
+
const messageText = rawMessage.slice(0, 400);
|
|
86
|
+
const nameText = rawName.slice(0, 400);
|
|
87
|
+
const display = messageText
|
|
88
|
+
? (nameText ? `${nameText}: ${messageText}` : messageText)
|
|
89
|
+
: nameText || (typeof error === "function" ? "Unknown function failure" : "Unknown object failure");
|
|
90
|
+
return formatBoundedCaughtDetail(display, rawMessage.length > messageText.length || rawName.length > nameText.length);
|
|
91
|
+
}
|
|
92
|
+
function formatBoundedCaughtDetail(value, alreadyTruncated = false) {
|
|
93
|
+
const inputWasTruncated = alreadyTruncated || value.length > 400;
|
|
94
|
+
const formatted = formatPermissionErrorDetail(value.slice(0, 400));
|
|
95
|
+
if (!inputWasTruncated || formatted.endsWith("…"))
|
|
96
|
+
return formatted;
|
|
97
|
+
return `${formatted.slice(0, 399)}…`;
|
|
98
|
+
}
|
|
99
|
+
function safeStderr(value) {
|
|
100
|
+
if (typeof value === "string")
|
|
101
|
+
return formatBoundedCaughtDetail(value);
|
|
102
|
+
try {
|
|
103
|
+
if (!isUint8Array(value))
|
|
104
|
+
return "";
|
|
105
|
+
const backing = Reflect.apply(typedArrayBufferGetter, value, []);
|
|
106
|
+
const byteOffset = Reflect.apply(typedArrayByteOffsetGetter, value, []);
|
|
107
|
+
const byteLength = Reflect.apply(typedArrayByteLengthGetter, value, []);
|
|
108
|
+
const copiedByteLength = byteLength < MAX_STDERR_BYTES
|
|
109
|
+
? byteLength : MAX_STDERR_BYTES;
|
|
110
|
+
const source = new Uint8ArrayIntrinsic(backing, byteOffset, copiedByteLength);
|
|
111
|
+
const copy = new Uint8ArrayIntrinsic(copiedByteLength);
|
|
112
|
+
Reflect.apply(typedArraySet, copy, [source, 0]);
|
|
113
|
+
const decoded = Reflect.apply(textDecoderDecode, stderrDecoder, [copy]);
|
|
114
|
+
return formatBoundedCaughtDetail(decoded, byteLength > copiedByteLength);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
11
120
|
function commandFailureFields(error) {
|
|
12
|
-
const
|
|
121
|
+
const killed = dataProperty(error, "killed");
|
|
122
|
+
const code = dataProperty(error, "code");
|
|
123
|
+
const signal = dataProperty(error, "signal");
|
|
124
|
+
const stderr = dataProperty(error, "stderr");
|
|
13
125
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
126
|
+
found: killed.found || code.found || signal.found || stderr.found,
|
|
127
|
+
timedOut: killed.found && killed.value === true &&
|
|
128
|
+
signal.found && signal.value === "SIGKILL",
|
|
129
|
+
exitCode: code.found && typeof code.value === "number" ? code.value : null,
|
|
130
|
+
signal: signal.found && typeof signal.value === "string" ? signal.value : null,
|
|
131
|
+
stderr: stderr.found ? safeStderr(stderr.value) : "",
|
|
20
132
|
};
|
|
21
133
|
}
|
|
22
134
|
export class PermissionCommandError extends Error {
|
|
@@ -41,21 +153,38 @@ export class PermissionCommandError extends Error {
|
|
|
41
153
|
}
|
|
42
154
|
}
|
|
43
155
|
export function getPermissionCommandFailure(error, command, durationMs) {
|
|
44
|
-
if (error
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
156
|
+
if (isInspectableCaughtObject(error) && utilTypes.isNativeError(error)) {
|
|
157
|
+
const name = dataProperty(error, "name");
|
|
158
|
+
if (name.found && name.value === "PermissionCommandError") {
|
|
159
|
+
const wrappedCommand = dataProperty(error, "command");
|
|
160
|
+
const wrappedDuration = dataProperty(error, "durationMs");
|
|
161
|
+
const wrappedTimedOut = dataProperty(error, "timedOut");
|
|
162
|
+
const wrappedExitCode = dataProperty(error, "exitCode");
|
|
163
|
+
const wrappedSignal = dataProperty(error, "signal");
|
|
164
|
+
const wrappedStderr = dataProperty(error, "stderr");
|
|
165
|
+
if (wrappedCommand.found && typeof wrappedCommand.value === "string" &&
|
|
166
|
+
wrappedDuration.found && typeof wrappedDuration.value === "number" &&
|
|
167
|
+
Number.isSafeInteger(wrappedDuration.value) && wrappedDuration.value >= 0 &&
|
|
168
|
+
wrappedTimedOut.found && typeof wrappedTimedOut.value === "boolean" &&
|
|
169
|
+
wrappedExitCode.found && (wrappedExitCode.value === null || typeof wrappedExitCode.value === "number") &&
|
|
170
|
+
wrappedSignal.found && (wrappedSignal.value === null || typeof wrappedSignal.value === "string") &&
|
|
171
|
+
wrappedStderr.found) {
|
|
172
|
+
return {
|
|
173
|
+
command: wrappedCommand.value,
|
|
174
|
+
durationMs: wrappedDuration.value,
|
|
175
|
+
timedOut: wrappedTimedOut.value,
|
|
176
|
+
exitCode: wrappedExitCode.value,
|
|
177
|
+
signal: wrappedSignal.value,
|
|
178
|
+
stderr: safeStderr(wrappedStderr.value),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
53
182
|
}
|
|
54
|
-
|
|
55
|
-
|
|
183
|
+
const fields = commandFailureFields(error);
|
|
184
|
+
if (!fields.found)
|
|
56
185
|
return undefined;
|
|
57
|
-
}
|
|
58
|
-
return { command, durationMs: Math.round(durationMs), ...
|
|
186
|
+
const { found: _found, ...detail } = fields;
|
|
187
|
+
return { command, durationMs: Math.round(durationMs), ...detail };
|
|
59
188
|
}
|
|
60
189
|
export async function executePermissionCommand(command, args, timeoutMs = DEFAULT_PERMISSION_EXEC_TIMEOUT_MS) {
|
|
61
190
|
const startedAt = performance.now();
|
|
@@ -96,7 +96,7 @@ export async function inspectWindowsPermissions(params) {
|
|
|
96
96
|
env: params.opts?.env,
|
|
97
97
|
exec: params.opts?.exec ?? defaultPermissionExec,
|
|
98
98
|
});
|
|
99
|
-
if (owner.error) {
|
|
99
|
+
if (owner.error !== undefined) {
|
|
100
100
|
const error = `Windows owner inspection failed: ${owner.error}`;
|
|
101
101
|
return { ...unverified, ownerError: owner.error, error, errorDetail: owner.errorDetail, errorCause: owner.errorCause };
|
|
102
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-mutation-admission.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-admission.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAKV,4BAA4B,EAC7B,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAgCtE,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,CAAC,CAAC;AAEH,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,gBAAgB,EAAE,qBAAqB,GAAG,SAAS,GAClD,4BAA4B,GAAG,SAAS,CAO1C;AAuJD,wBAAsB,mCAAmC,CAAC,MAAM,EAAE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACjD,cAAc,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,GAAG,OAAO,CAAC;IACV,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD,CAAC,
|
|
1
|
+
{"version":3,"file":"pinned-mutation-admission.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-admission.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAKV,4BAA4B,EAC7B,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAgCtE,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,CAAC,CAAC;AAEH,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,gBAAgB,EAAE,qBAAqB,GAAG,SAAS,GAClD,4BAA4B,GAAG,SAAS,CAO1C;AAuJD,wBAAsB,mCAAmC,CAAC,MAAM,EAAE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACjD,cAAc,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,GAAG,OAAO,CAAC;IACV,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD,CAAC,CAgQD"}
|
|
@@ -7,7 +7,7 @@ import { ordinarySharedAbsoluteInsideRoot, simpleSharedRoute, } from "./pinned-m
|
|
|
7
7
|
import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
|
|
8
8
|
import { outsideWorkspaceError } from "./root-errors.js";
|
|
9
9
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
10
|
-
import { advanceMutationObservation, mutationObservationCurrent, observeMutationPath,
|
|
10
|
+
import { advanceMutationObservation, mutationObservationCurrent, observeMutationPath, mutationObservationsCurrent, mutationObservationUsesDirectory, missingMutationSegments, nextMissingMutationPath, } from "./pinned-mutation-observation.js";
|
|
11
11
|
export function snapshotPinnedMutationPolicy(denyMutations, mutationSymlinks) {
|
|
12
12
|
if (denyMutations === undefined && mutationSymlinks === undefined)
|
|
13
13
|
return undefined;
|
|
@@ -92,10 +92,12 @@ function captureEpoch(rootReal, target, policy, session) {
|
|
|
92
92
|
return undefined;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
function epochCurrent(epoch) {
|
|
95
|
+
function epochCurrent(epoch, directories) {
|
|
96
96
|
return !process.versions.bun && epoch.mode === epoch.session.nativeMode &&
|
|
97
97
|
getFsSafeNativeConfig().mode === epoch.mode &&
|
|
98
|
-
epoch.observations
|
|
98
|
+
(directories ? mutationObservationsCurrent(epoch.observations, directories) :
|
|
99
|
+
epoch.observations.every(mutationObservationCurrent)) &&
|
|
100
|
+
getFsSafeNativeConfig().mode === epoch.mode;
|
|
99
101
|
}
|
|
100
102
|
function assertCachedNotDenied(target, epoch) {
|
|
101
103
|
if (epoch.paths.some((paths) => paths.some((denied) => isPathInside(denied, target) && isPathInside(target, denied))) ||
|
|
@@ -143,10 +145,8 @@ function advanceAuthorizedDirectory(admittedEpoch, admitted, receipt) {
|
|
|
143
145
|
target,
|
|
144
146
|
observations: Object.freeze(complete),
|
|
145
147
|
});
|
|
146
|
-
if (!
|
|
147
|
-
!
|
|
148
|
-
!mutationObservationUsesDirectory(target, receipt.child) ||
|
|
149
|
-
!epochCurrent(next))
|
|
148
|
+
if (!mutationObservationUsesDirectory(target, receipt.child) ||
|
|
149
|
+
!epochCurrent(next, [receipt.parent, receipt.child]))
|
|
150
150
|
return undefined;
|
|
151
151
|
assertCachedNotDenied(target.path, next);
|
|
152
152
|
assertCachedNotDenied(receipt.child.path, next);
|
|
@@ -165,6 +165,10 @@ export async function preparePinnedWriteMutationAdmission(params) {
|
|
|
165
165
|
selectedTarget: params.resolvedTargetPath,
|
|
166
166
|
policy,
|
|
167
167
|
});
|
|
168
|
+
const nativeRoute = sharedRoute ?? (process.platform === "win32" && params.originalPath?.includes("/")
|
|
169
|
+
? simpleSharedRoute({ rootReal: params.rootReal, rootIdentity: params.rootIdentity,
|
|
170
|
+
originalPath: params.originalPath.replaceAll("/", "\\"), selectedTarget: params.resolvedTargetPath, policy })
|
|
171
|
+
: undefined);
|
|
168
172
|
let pinnedSession;
|
|
169
173
|
let epoch;
|
|
170
174
|
let pending;
|
|
@@ -194,13 +198,13 @@ export async function preparePinnedWriteMutationAdmission(params) {
|
|
|
194
198
|
}
|
|
195
199
|
return current;
|
|
196
200
|
};
|
|
197
|
-
const
|
|
201
|
+
const createSharedParentWalk = (admittedRoute) => {
|
|
198
202
|
const session = Object.freeze({
|
|
199
203
|
kind: "shared",
|
|
200
|
-
route:
|
|
204
|
+
route: admittedRoute.route,
|
|
201
205
|
selectedTarget: params.resolvedTargetPath,
|
|
202
206
|
nativeMode: getFsSafeNativeConfig().mode,
|
|
203
|
-
rootIdentity:
|
|
207
|
+
rootIdentity: admittedRoute.rootIdentity,
|
|
204
208
|
authorizationToken: Object.freeze({}),
|
|
205
209
|
});
|
|
206
210
|
let disposed = false;
|
|
@@ -232,20 +236,14 @@ export async function preparePinnedWriteMutationAdmission(params) {
|
|
|
232
236
|
if (!reusable || reusable.session !== session ||
|
|
233
237
|
!reusableSharedCreateRequest(request, reusable) ||
|
|
234
238
|
!mutationObservationUsesDirectory(reusable.target, parent) ||
|
|
235
|
-
!epochCurrent(reusable)) {
|
|
239
|
+
!nonzeroDirectoryObservation(parent) || !epochCurrent(reusable, [parent])) {
|
|
236
240
|
disable();
|
|
237
241
|
return undefined;
|
|
238
242
|
}
|
|
239
243
|
assertCachedNotDenied(request.targetPath, reusable);
|
|
240
244
|
assertCachedNotDenied(request.mutationPath, reusable);
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// still match the observation captured by this walk.
|
|
244
|
-
if (!nonzeroDirectoryObservation(parent) ||
|
|
245
|
-
!mutationDirectoryObservationCurrent(parent)) {
|
|
246
|
-
disable();
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
245
|
+
// The epoch check ends with the exact parent fence. The receipt is
|
|
246
|
+
// consumed synchronously before any intervening authority callback.
|
|
249
247
|
sharedPending = Object.freeze({
|
|
250
248
|
epoch: reusable,
|
|
251
249
|
childPath: request.mutationPath,
|
|
@@ -320,7 +318,7 @@ export async function preparePinnedWriteMutationAdmission(params) {
|
|
|
320
318
|
disable();
|
|
321
319
|
},
|
|
322
320
|
});
|
|
323
|
-
}
|
|
321
|
+
};
|
|
324
322
|
const mutationAdmission = Object.freeze({
|
|
325
323
|
rejectParentSymlinks: policy.mutationSymlinks === "reject",
|
|
326
324
|
beginParentWalk: route ? () => {
|
|
@@ -335,26 +333,22 @@ export async function preparePinnedWriteMutationAdmission(params) {
|
|
|
335
333
|
pending = undefined;
|
|
336
334
|
return route;
|
|
337
335
|
} : undefined,
|
|
338
|
-
beginSharedParentWalk,
|
|
336
|
+
beginSharedParentWalk: sharedRoute ? () => createSharedParentWalk(sharedRoute) : undefined,
|
|
337
|
+
beginNativeParentWalk: nativeRoute ? () => createSharedParentWalk(nativeRoute) : undefined,
|
|
339
338
|
tryAuthorizeAtParent(request, parent) {
|
|
340
339
|
pending = undefined;
|
|
341
340
|
const reusable = epoch;
|
|
342
341
|
if (!reusable || reusable.session !== pinnedSession ||
|
|
343
342
|
!reusableRequest(request, reusable) ||
|
|
344
343
|
!mutationObservationUsesDirectory(reusable.target, parent) ||
|
|
345
|
-
!epochCurrent(reusable)) {
|
|
344
|
+
!epochCurrent(reusable, [parent])) {
|
|
346
345
|
epoch = undefined;
|
|
347
346
|
return undefined;
|
|
348
347
|
}
|
|
349
348
|
assertCachedNotDenied(request.targetPath, reusable);
|
|
350
349
|
assertCachedNotDenied(request.mutationPath, reusable);
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// spelling, descriptor identity, type, mode, and link count still match.
|
|
354
|
-
if (!mutationDirectoryObservationCurrent(parent)) {
|
|
355
|
-
epoch = undefined;
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
350
|
+
// The epoch check ends at the exact parent pathname, canonical spelling,
|
|
351
|
+
// identity, type, mode and link-count fence, with no intervening await.
|
|
358
352
|
if (request.phase === "parent-create") {
|
|
359
353
|
pending = Object.freeze({
|
|
360
354
|
epoch: reusable,
|
|
@@ -6,6 +6,11 @@ export type MutationIdentity = Readonly<{
|
|
|
6
6
|
nlink: bigint;
|
|
7
7
|
}>;
|
|
8
8
|
declare const checkedDirectoryObservation: unique symbol;
|
|
9
|
+
export type MutationDirectoryCurrent = Readonly<{
|
|
10
|
+
canonicalPath: string;
|
|
11
|
+
identity: MutationIdentity;
|
|
12
|
+
}>;
|
|
13
|
+
export type MutationDirectoryObserver = () => MutationDirectoryCurrent | undefined;
|
|
9
14
|
export type MutationDirectoryObservation = Readonly<{
|
|
10
15
|
path: string;
|
|
11
16
|
canonicalPath: string;
|
|
@@ -24,11 +29,11 @@ export type MutationPathObservation = Readonly<{
|
|
|
24
29
|
}>;
|
|
25
30
|
export declare function missingMutationSegments(observation: MutationPathObservation): number;
|
|
26
31
|
export declare function nextMissingMutationPath(observation: MutationPathObservation): string | undefined;
|
|
27
|
-
export declare function checkedMutationDirectory(pathname: string, canonicalPath: string, stat: BigIntStats): MutationDirectoryObservation;
|
|
28
|
-
export declare function mutationDirectoryObservationCurrent(observation: MutationDirectoryObservation): boolean;
|
|
32
|
+
export declare function checkedMutationDirectory(pathname: string, canonicalPath: string, stat: Pick<BigIntStats, "dev" | "ino" | "mode" | "nlink">, observeCurrent?: MutationDirectoryObserver): MutationDirectoryObservation;
|
|
29
33
|
export declare function mutationObservationUsesDirectory(observation: MutationPathObservation, directory: MutationDirectoryObservation): boolean;
|
|
30
34
|
export declare function observeMutationPath(pathname: string): MutationPathObservation | undefined;
|
|
31
35
|
export declare function mutationObservationCurrent(observation: MutationPathObservation): boolean;
|
|
36
|
+
export declare function mutationObservationsCurrent(observations: readonly MutationPathObservation[], directories: readonly MutationDirectoryObservation[]): boolean;
|
|
32
37
|
export declare function advanceMutationObservation(observation: MutationPathObservation, parent: MutationDirectoryObservation, child: MutationDirectoryObservation): MutationPathObservation | undefined;
|
|
33
38
|
export {};
|
|
34
39
|
//# sourceMappingURL=pinned-mutation-observation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-mutation-observation.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-observation.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,QAAA,MAAM,2BAA2B,eAAmD,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-mutation-observation.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-observation.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,QAAA,MAAM,2BAA2B,eAAmD,CAAC;AAGrF,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,gBAAgB,CAAC;CAC5B,CAAC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,wBAAwB,GAAG,SAAS,CAAC;AAEnF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,CAAC,2BAA2B,CAAC,EAAE,IAAI,CAAC;CACrC,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AA6BH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,CAEpF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAGhG;AAKD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,EACzD,cAAc,CAAC,EAAE,yBAAyB,GACzC,4BAA4B,CAY9B;AAQD,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,uBAAuB,EACpC,SAAS,EAAE,4BAA4B,GACtC,OAAO,CAMT;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CA0BzF;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,uBAAuB,GAAG,OAAO,CAaxF;AAKD,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,SAAS,uBAAuB,EAAE,EAChD,WAAW,EAAE,SAAS,4BAA4B,EAAE,GACnD,OAAO,CAiET;AAID,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,uBAAuB,EACpC,MAAM,EAAE,4BAA4B,EACpC,KAAK,EAAE,4BAA4B,GAClC,uBAAuB,GAAG,SAAS,CAiBrC"}
|
|
@@ -4,6 +4,7 @@ import { isNotFoundPathError } from "./path.js";
|
|
|
4
4
|
import { realpathSync } from "./realpath.js";
|
|
5
5
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
6
|
const checkedDirectoryObservation = Symbol("checked mutation directory observation");
|
|
7
|
+
const directoryObservers = new WeakMap();
|
|
7
8
|
function identity(stat) {
|
|
8
9
|
return Object.freeze({ dev: stat.dev, ino: stat.ino, mode: stat.mode, nlink: stat.nlink });
|
|
9
10
|
}
|
|
@@ -17,8 +18,7 @@ function directoryMode(mode) {
|
|
|
17
18
|
}
|
|
18
19
|
function absent(pathname) {
|
|
19
20
|
try {
|
|
20
|
-
fs.lstatSync(pathname);
|
|
21
|
-
return false;
|
|
21
|
+
return fs.lstatSync(pathname, { throwIfNoEntry: false }) === undefined;
|
|
22
22
|
}
|
|
23
23
|
catch (error) {
|
|
24
24
|
// ENOTDIR is incomplete evidence, not a missing child of a directory.
|
|
@@ -35,35 +35,25 @@ export function nextMissingMutationPath(observation) {
|
|
|
35
35
|
// Callers may construct this only from their just-completed exact pathname and
|
|
36
36
|
// canonical fences. The private brand prevents unverified structural objects
|
|
37
37
|
// from advancing an admission epoch.
|
|
38
|
-
export function checkedMutationDirectory(pathname, canonicalPath, stat) {
|
|
39
|
-
if (!
|
|
38
|
+
export function checkedMutationDirectory(pathname, canonicalPath, stat, observeCurrent) {
|
|
39
|
+
if (!directoryMode(stat.mode)) {
|
|
40
40
|
throw new TypeError("mutation directory evidence must describe a real directory");
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
const observation = Object.freeze({
|
|
43
43
|
path: pathname,
|
|
44
44
|
canonicalPath,
|
|
45
45
|
identity: identity(stat),
|
|
46
46
|
[checkedDirectoryObservation]: true,
|
|
47
47
|
});
|
|
48
|
+
if (observeCurrent)
|
|
49
|
+
directoryObservers.set(observation, observeCurrent);
|
|
50
|
+
return observation;
|
|
48
51
|
}
|
|
49
52
|
function checkedDirectory(observation) {
|
|
50
53
|
return observation[checkedDirectoryObservation] === true &&
|
|
51
54
|
path.isAbsolute(observation.path) && path.isAbsolute(observation.canonicalPath) &&
|
|
52
55
|
directoryMode(observation.identity.mode);
|
|
53
56
|
}
|
|
54
|
-
export function mutationDirectoryObservationCurrent(observation) {
|
|
55
|
-
if (!checkedDirectory(observation))
|
|
56
|
-
return false;
|
|
57
|
-
try {
|
|
58
|
-
const stat = inspectFileIdentitySync(() => fs.lstatSync(observation.path, { bigint: true }));
|
|
59
|
-
return stat.isDirectory() && !stat.isSymbolicLink() &&
|
|
60
|
-
sameIdentity(observation.identity, stat) &&
|
|
61
|
-
realpathSync.native(observation.path) === observation.canonicalPath;
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
57
|
export function mutationObservationUsesDirectory(observation, directory) {
|
|
68
58
|
return checkedDirectory(directory) &&
|
|
69
59
|
observation.ancestor === directory.path &&
|
|
@@ -118,6 +108,79 @@ export function mutationObservationCurrent(observation) {
|
|
|
118
108
|
return false;
|
|
119
109
|
}
|
|
120
110
|
}
|
|
111
|
+
// Share only ordinary spelling observations collected in this synchronous
|
|
112
|
+
// phase. Aliases retain their independent raw/canonical binding checks, and no
|
|
113
|
+
// fact escapes this call or crosses an authority callback or mutation.
|
|
114
|
+
export function mutationObservationsCurrent(observations, directories) {
|
|
115
|
+
const groups = new Map();
|
|
116
|
+
const groupFor = (pathname) => {
|
|
117
|
+
let group = groups.get(pathname);
|
|
118
|
+
if (!group) {
|
|
119
|
+
group = { observations: [], directories: [] };
|
|
120
|
+
groups.set(pathname, group);
|
|
121
|
+
}
|
|
122
|
+
return group;
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
for (const observation of observations) {
|
|
126
|
+
if (observation.ancestor !== observation.canonicalAncestor) {
|
|
127
|
+
if (!mutationObservationCurrent(observation))
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
groupFor(observation.ancestor).observations.push(observation);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const directory of directories) {
|
|
135
|
+
if (!checkedDirectory(directory))
|
|
136
|
+
return false;
|
|
137
|
+
groupFor(directory.path).directories.push(directory);
|
|
138
|
+
}
|
|
139
|
+
const current = (pathname, group) => {
|
|
140
|
+
const missing = new Set(group.observations.flatMap((observation) => {
|
|
141
|
+
const next = nextMissingMutationPath(observation);
|
|
142
|
+
return next === undefined ? [] : [next];
|
|
143
|
+
}));
|
|
144
|
+
// Exact directory roles are the end fence. Observe missing children
|
|
145
|
+
// before refreshing that parent, matching the ordered admission fence.
|
|
146
|
+
if (group.directories.length && ![...missing].every(absent))
|
|
147
|
+
return false;
|
|
148
|
+
const observed = group.directories.length
|
|
149
|
+
? directoryObservers.get(group.directories[0])?.() : undefined;
|
|
150
|
+
const stat = observed?.identity ??
|
|
151
|
+
inspectFileIdentitySync(() => fs.lstatSync(pathname, { bigint: true }));
|
|
152
|
+
const canonical = observed?.canonicalPath ?? realpathSync.native(pathname);
|
|
153
|
+
const isDirectory = directoryMode(stat.mode);
|
|
154
|
+
if (!group.observations.every((observation) => canonical === observation.canonicalAncestor &&
|
|
155
|
+
sameIdentity(observation.entry, stat, isDirectory) &&
|
|
156
|
+
sameIdentity(observation.identity, stat, isDirectory) &&
|
|
157
|
+
(missingMutationSegments(observation) === 0 || isDirectory)))
|
|
158
|
+
return false;
|
|
159
|
+
if (!group.directories.every((directory) => isDirectory &&
|
|
160
|
+
sameIdentity(directory.identity, stat) && canonical === directory.canonicalPath))
|
|
161
|
+
return false;
|
|
162
|
+
return group.directories.length > 0 || [...missing].every(absent);
|
|
163
|
+
};
|
|
164
|
+
for (const [pathname, group] of groups) {
|
|
165
|
+
if (!group.directories.length && !current(pathname, group))
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
// Preserve the supplied parent/child order, even when a directory also
|
|
169
|
+
// appeared earlier as the Root or a denied path's nearest ancestor.
|
|
170
|
+
const checked = new Set();
|
|
171
|
+
for (const directory of directories) {
|
|
172
|
+
if (checked.has(directory.path))
|
|
173
|
+
continue;
|
|
174
|
+
if (!current(directory.path, groups.get(directory.path)))
|
|
175
|
+
return false;
|
|
176
|
+
checked.add(directory.path);
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
121
184
|
// This is deliberately a pure tentative update. The immediately following
|
|
122
185
|
// parent authorization validates the complete epoch once before reusing it.
|
|
123
186
|
export function advanceMutationObservation(observation, parent, child) {
|
|
@@ -9,6 +9,7 @@ type SharedMutationPolicy = Readonly<{
|
|
|
9
9
|
denyMutations?: DenyMutationPolicy;
|
|
10
10
|
mutationSymlinks?: MutationSymlinkPolicy;
|
|
11
11
|
}>;
|
|
12
|
+
export declare function ordinaryWindowsSegments(relativePath: string): boolean;
|
|
12
13
|
export declare function ordinarySharedAbsoluteInsideRoot(rootReal: string, candidatePath: string, rootIdentity: ExactRootIdentity): boolean;
|
|
13
14
|
export declare function simpleSharedRoute(params: {
|
|
14
15
|
rootReal: string;
|