@openclaw/matrix 2026.10.1-beta.2 → 2026.10.2
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/node_modules/@openclaw/fs-safe/CHANGELOG.md +56 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +218 -0
- package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +48 -5
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +35 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +61 -1
- package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
- package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
- package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +11 -11
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
|
@@ -59,12 +59,14 @@ import path from "node:path";
|
|
|
59
59
|
import { FsSafeError } from "./errors.js";
|
|
60
60
|
import { assertSynchronousCallbackResult, isMutationAuthorityError } from "./mutation-authority.js";
|
|
61
61
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
62
|
-
import { writePinnedInput } from "./pinned-write-input.js";
|
|
62
|
+
import { replayPinnedInput, writePinnedInput } from "./pinned-write-input.js";
|
|
63
63
|
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
64
64
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
65
65
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
66
66
|
import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
67
67
|
import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
|
|
68
|
+
import { noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
|
|
69
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
68
70
|
export function assertNativeStaging(binding) {
|
|
69
71
|
if ([
|
|
70
72
|
binding.closeOwnedFd,
|
|
@@ -93,6 +95,9 @@ class NativeStagedFile {
|
|
|
93
95
|
#state = { status: "open", publication: NOT_PUBLISHED };
|
|
94
96
|
#receipt;
|
|
95
97
|
#rejectFinalSymlink = false;
|
|
98
|
+
#unsupportedNoReplace;
|
|
99
|
+
#fallbackCleanup;
|
|
100
|
+
#fallbackCleanupError;
|
|
96
101
|
constructor(owner) {
|
|
97
102
|
const name = owner.name ?? `.fs-safe-${randomUUID()}.tmp`;
|
|
98
103
|
assertBasename(name, owner.portableNames !== false);
|
|
@@ -121,7 +126,7 @@ class NativeStagedFile {
|
|
|
121
126
|
await staged.#prepare(input, maxBytes);
|
|
122
127
|
return staged;
|
|
123
128
|
}
|
|
124
|
-
static async write(binding, parentFd, closeParentFd, directory, params, parentGuard) {
|
|
129
|
+
static async write(binding, parentFd, closeParentFd, directory, params, parentGuard, fallback) {
|
|
125
130
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
126
131
|
try {
|
|
127
132
|
const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
|
|
@@ -143,9 +148,33 @@ class NativeStagedFile {
|
|
|
143
148
|
if (staged.#verifyMode)
|
|
144
149
|
staged.#assertCurrent();
|
|
145
150
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
let published;
|
|
152
|
+
try {
|
|
153
|
+
published = exclusive
|
|
154
|
+
? staged.#completePublication(params.basename, false, params.onPublished)
|
|
155
|
+
: await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!staged.#unsupportedNoReplace || isFsSafeNativeRequired() ||
|
|
159
|
+
(params.input.kind === "file" && params.input.clone === "always"))
|
|
160
|
+
throw error;
|
|
161
|
+
// Remove only our identity-known stage before re-admitting the JS write.
|
|
162
|
+
// Keep its descriptor alive to replay a one-shot stream in bounded chunks.
|
|
163
|
+
staged.#fallbackCleanup = "failed";
|
|
164
|
+
let removed;
|
|
165
|
+
try {
|
|
166
|
+
removed = binding.removeStagedFile(parentFd, staged.#owner.name, staged.#file());
|
|
167
|
+
staged.#fallbackCleanup = removed;
|
|
168
|
+
}
|
|
169
|
+
catch (cleanupError) {
|
|
170
|
+
staged.#fallbackCleanupError = { error: cleanupError };
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (removed !== "removed" && removed !== "name-absent")
|
|
174
|
+
throw error;
|
|
175
|
+
rememberNoReplaceUnavailable(binding, parentFd, staged.#unsupportedNoReplace);
|
|
176
|
+
return await fallback(replayPinnedInput(staged.#file(), params.input));
|
|
177
|
+
}
|
|
149
178
|
const identity = published.staged.identity;
|
|
150
179
|
try {
|
|
151
180
|
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
@@ -321,6 +350,15 @@ class NativeStagedFile {
|
|
|
321
350
|
catch (error) {
|
|
322
351
|
// Record before inspecting metadata, whose getters can reenter cleanup.
|
|
323
352
|
state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
|
|
353
|
+
// Linux VFS and rename(2): unsupported flags return EINVAL; ENOSYS
|
|
354
|
+
// means no syscall. A regular file renamed to a distinct sibling cannot
|
|
355
|
+
// hit the directory-into-itself EINVAL case. Nothing was published.
|
|
356
|
+
const unavailable = !overwrite ? noReplaceUnavailable(error, "publication", true) : undefined;
|
|
357
|
+
if (unavailable && this.#state === state && state.status === "open") {
|
|
358
|
+
state.publication = NOT_PUBLISHED;
|
|
359
|
+
this.#unsupportedNoReplace = unavailable;
|
|
360
|
+
throw unavailable;
|
|
361
|
+
}
|
|
324
362
|
// Only explicit pre-dispatch provenance can rule out a committed rename.
|
|
325
363
|
if (classifyNativeRenameFailure(error) === "uncommitted") {
|
|
326
364
|
state.publication = NOT_PUBLISHED;
|
|
@@ -377,6 +415,11 @@ class NativeStagedFile {
|
|
|
377
415
|
if (state.publication.status === "indeterminate") {
|
|
378
416
|
outcome = "preserved";
|
|
379
417
|
}
|
|
418
|
+
else if (this.#fallbackCleanup) {
|
|
419
|
+
outcome = this.#fallbackCleanup;
|
|
420
|
+
if (this.#fallbackCleanupError)
|
|
421
|
+
errors.push(this.#fallbackCleanupError.error);
|
|
422
|
+
}
|
|
380
423
|
else if (state.publication.status === "not-published" && state.fileFd !== undefined) {
|
|
381
424
|
try {
|
|
382
425
|
outcome = this.#owner.binding.removeStagedFile(this.#owner.parentFd, this.#owner.name, state.fileFd);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import type { PinnedWriteInput } from "./pinned-write-types.js";
|
|
3
|
+
/** Replay a consumed stream from an owned, readable stage descriptor. */
|
|
4
|
+
export declare function replayPinnedInput(fd: number, input: PinnedWriteInput): PinnedWriteInput;
|
|
3
5
|
export declare function writePinnedInput(target: FileHandle | number, input: PinnedWriteInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import { readSync } from "node:fs";
|
|
1
2
|
import { types } from "node:util";
|
|
2
3
|
import { snapshotByteView } from "./byte-view.js";
|
|
3
4
|
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { writeAllToFile } from "./write-file-handle.js";
|
|
7
|
+
/** Replay a consumed stream from an owned, readable stage descriptor. */
|
|
8
|
+
export function replayPinnedInput(fd, input) {
|
|
9
|
+
if (input.kind !== "stream")
|
|
10
|
+
return input;
|
|
11
|
+
return { kind: "stream", stageBeforePublish: true, stream: (async function* () {
|
|
12
|
+
let position = 0;
|
|
13
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
14
|
+
for (;;) {
|
|
15
|
+
const bytes = readSync(fd, buffer, 0, buffer.length, position);
|
|
16
|
+
if (!bytes)
|
|
17
|
+
return;
|
|
18
|
+
position += bytes;
|
|
19
|
+
yield buffer.subarray(0, bytes);
|
|
20
|
+
}
|
|
21
|
+
})() };
|
|
22
|
+
}
|
|
6
23
|
const isUint8Array = types.isUint8Array;
|
|
7
24
|
function streamChunkBytes(chunk) {
|
|
8
25
|
if (Buffer.isBuffer(chunk))
|
|
@@ -90,7 +90,7 @@ export async function runPinnedWriteHelper(params) {
|
|
|
90
90
|
}
|
|
91
91
|
const native = getNativeBinding();
|
|
92
92
|
if (native) {
|
|
93
|
-
return await runPinnedWriteNative(native, normalizedParams);
|
|
93
|
+
return await runPinnedWriteNative(native, normalizedParams, input => runPinnedWriteFallback({ ...normalizedParams, input }));
|
|
94
94
|
}
|
|
95
95
|
return await runPinnedWriteFallback(normalizedParams);
|
|
96
96
|
}
|
|
@@ -13,6 +13,9 @@ export type PublishFileExclusiveFailureDetails = {
|
|
|
13
13
|
targetIdentity?: FileIdentityStat;
|
|
14
14
|
cleanup: PublishFileExclusiveCleanup;
|
|
15
15
|
directorySync?: PublishFileExclusiveDirectorySyncFailure;
|
|
16
|
+
fallback?: "link-unlink";
|
|
17
|
+
publication?: "published";
|
|
18
|
+
sourceRemoval?: "not-attempted" | "still-linked" | "unverified" | "removed";
|
|
16
19
|
};
|
|
17
20
|
export type PublishFailureState = {
|
|
18
21
|
phase: PublishFileExclusiveFailurePhase;
|
|
@@ -21,6 +24,8 @@ export type PublishFailureState = {
|
|
|
21
24
|
targetCleanupIdentity?: FileIdentityStat;
|
|
22
25
|
preserveTarget: boolean;
|
|
23
26
|
directorySync?: PublishFileExclusiveDirectorySyncFailure;
|
|
27
|
+
fallback?: PublishFileExclusiveFailureDetails["fallback"];
|
|
28
|
+
sourceRemoval?: PublishFileExclusiveFailureDetails["sourceRemoval"];
|
|
24
29
|
};
|
|
25
30
|
export declare function rememberCreatedTarget(state: PublishFailureState, identity: FileIdentityStat, phase: PublishFileExclusiveFailurePhase): void;
|
|
26
31
|
export declare function publicationFailure(error: unknown, state: PublishFailureState, cleanup: PublishFileExclusiveCleanup): FsSafeError;
|
|
@@ -12,6 +12,7 @@ export function publicationFailure(error, state, cleanup) {
|
|
|
12
12
|
targetCreated: state.targetCreated,
|
|
13
13
|
...(state.targetIdentity ? { targetIdentity: state.targetIdentity } : {}),
|
|
14
14
|
...(state.directorySync ? { directorySync: state.directorySync } : {}),
|
|
15
|
+
...(state.fallback ? { fallback: state.fallback, publication: "published", sourceRemoval: state.sourceRemoval } : {}),
|
|
15
16
|
cleanup,
|
|
16
17
|
};
|
|
17
18
|
return new FsSafeError(error instanceof FsSafeError ? error.code : "helper-failed", `exclusive file publication failed during ${state.phase}: ${cause.message}`, { cause, details });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
3
|
+
import { type PublishFailureState } from "./publish-file-failure.js";
|
|
4
|
+
export declare function publishFileNoReplaceNative(binding: NativeBinding, paths: Parameters<NativeBinding["renameNoReplace"]>, expected: BigIntStats, failure: PublishFailureState): "link-unlink" | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
3
|
+
import { nativeMoveNoReplace } from "./native-move-fallback.js";
|
|
4
|
+
import { rememberCreatedTarget } from "./publish-file-failure.js";
|
|
5
|
+
export function publishFileNoReplaceNative(binding, paths, expected, failure) {
|
|
6
|
+
try {
|
|
7
|
+
const fallback = nativeMoveNoReplace(binding, paths, expected, getFsSafeNativeConfig().mode === "auto", "file publication");
|
|
8
|
+
if (fallback) {
|
|
9
|
+
failure.fallback = fallback;
|
|
10
|
+
failure.sourceRemoval = "removed";
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof FsSafeError && error.details?.publication === "published") {
|
|
16
|
+
failure.targetCreated = true;
|
|
17
|
+
failure.preserveTarget = true;
|
|
18
|
+
failure.phase = "rename-verify";
|
|
19
|
+
failure.fallback = "link-unlink";
|
|
20
|
+
failure.sourceRemoval = error.details.sourceRemoval;
|
|
21
|
+
// Changed/unverified pairs cannot establish which inode was published.
|
|
22
|
+
if (failure.sourceRemoval === "still-linked")
|
|
23
|
+
rememberCreatedTarget(failure, expected, "rename-verify");
|
|
24
|
+
}
|
|
25
|
+
if (error instanceof FsSafeError)
|
|
26
|
+
throw error;
|
|
27
|
+
const code = error?.code;
|
|
28
|
+
if (code === "path-mismatch" || code === "hardlink") {
|
|
29
|
+
throw new FsSafeError(code, "publication source changed before fallback", { cause: error });
|
|
30
|
+
}
|
|
31
|
+
if (code === "ELOOP")
|
|
32
|
+
throw new FsSafeError("symlink", "publication source became a symlink", { cause: error });
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -5,6 +5,8 @@ import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failu
|
|
|
5
5
|
export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
|
|
6
6
|
export type PublishFileExclusiveResult = {
|
|
7
7
|
method: "hardlink" | "exclusive-copy" | "rename-noreplace";
|
|
8
|
+
/** Linux auto fallback; exclusive and identity-checked, but not crash-atomic. */
|
|
9
|
+
fallback?: "link-unlink";
|
|
8
10
|
identity: Stats;
|
|
9
11
|
directorySync: DirectorySyncOutcome;
|
|
10
12
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publishFileNoReplaceNative } from "./publish-file-native.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import fsSync, {} from "node:fs";
|
|
3
4
|
import fs, {} from "node:fs/promises";
|
|
@@ -256,7 +257,7 @@ export async function publishFileExclusive(params) {
|
|
|
256
257
|
else
|
|
257
258
|
assertSourceCurrent();
|
|
258
259
|
assertPublishedTargetCurrent(targetPath, identity, "publication target changed during directory sync");
|
|
259
|
-
return { method, identity: fsSync.fstatSync(handle.fd), directorySync };
|
|
260
|
+
return { method, ...(failure.fallback ? { fallback: failure.fallback } : {}), identity: fsSync.fstatSync(handle.fd), directorySync };
|
|
260
261
|
};
|
|
261
262
|
await parent.assertCurrent();
|
|
262
263
|
assertSourceCurrent();
|
|
@@ -269,7 +270,12 @@ export async function publishFileExclusive(params) {
|
|
|
269
270
|
}
|
|
270
271
|
if (strategy === "rename-noreplace") {
|
|
271
272
|
const binding = requireNativeBinding();
|
|
272
|
-
binding
|
|
273
|
+
publishFileNoReplaceNative(binding, [
|
|
274
|
+
sourceNativeParent.handle.fd,
|
|
275
|
+
sourceNativeParent.basename,
|
|
276
|
+
targetNativeParent.handle.fd,
|
|
277
|
+
targetNativeParent.basename,
|
|
278
|
+
], sourceExactIdentity, failure);
|
|
273
279
|
rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
|
|
274
280
|
// A failed post-rename fence must not delete the only remaining name.
|
|
275
281
|
failure.preserveTarget = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { noReplaceUnavailable } from "./native-noreplace.js";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import fsSync from "node:fs";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
@@ -19,7 +20,8 @@ function assertGuardsCurrent(guards) {
|
|
|
19
20
|
}
|
|
20
21
|
function replacementFailure(error, message, details, cause = error) {
|
|
21
22
|
if (error instanceof FsSafeError) {
|
|
22
|
-
|
|
23
|
+
const code = error.code;
|
|
24
|
+
return new FsSafeError(code, code === "helper-unavailable" ? `${message}: ${error.message}` : message, { cause, details });
|
|
23
25
|
}
|
|
24
26
|
const failure = new Error(message, { cause });
|
|
25
27
|
copyOperationalCode(failure, error);
|
|
@@ -46,14 +48,13 @@ function observeOptionalDirectory(pathname) {
|
|
|
46
48
|
return inspectDirectoryIdentitySync(pathname, undefined, initial);
|
|
47
49
|
}
|
|
48
50
|
function normalizeNativeRenameFailure(error) {
|
|
51
|
+
const unavailable = noReplaceUnavailable(error, "directory rename", true);
|
|
52
|
+
if (unavailable)
|
|
53
|
+
return unavailable;
|
|
49
54
|
const code = error?.code;
|
|
50
55
|
if (code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH) {
|
|
51
56
|
return new FsSafeError("path-mismatch", "directory replacement source identity changed before native rename", { cause: error });
|
|
52
57
|
}
|
|
53
|
-
if (code === "EINVAL" || code === "ENOSYS" || code === "ENOTSUP" ||
|
|
54
|
-
code === "EOPNOTSUPP") {
|
|
55
|
-
return new FsSafeError("helper-unavailable", "native no-replace directory rename is unavailable on this filesystem", { cause: error });
|
|
56
|
-
}
|
|
57
58
|
return error;
|
|
58
59
|
}
|
|
59
60
|
function renameRetainedDirectory(params) {
|
|
@@ -29,7 +29,7 @@ export function hardlinkedPathNotAllowedError() {
|
|
|
29
29
|
export function isAlreadyExistsError(error) {
|
|
30
30
|
return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
|
|
31
31
|
}
|
|
32
|
-
function
|
|
32
|
+
function resourceExhaustion(error) {
|
|
33
33
|
const pending = [error];
|
|
34
34
|
let details;
|
|
35
35
|
let code;
|
|
@@ -45,7 +45,11 @@ function descriptorExhaustion(error) {
|
|
|
45
45
|
const diagnostic = current;
|
|
46
46
|
if (current instanceof FsSafeError && (!details || current.details?.cleanup))
|
|
47
47
|
details = current.details ?? details;
|
|
48
|
-
if (diagnostic.code === "EMFILE" || diagnostic.code === "ENFILE")
|
|
48
|
+
if (diagnostic.code === "EMFILE" || diagnostic.code === "ENFILE") {
|
|
49
|
+
if (!code || code === "ENOSPC")
|
|
50
|
+
code = diagnostic.code;
|
|
51
|
+
}
|
|
52
|
+
else if (diagnostic.code === "ENOSPC")
|
|
49
53
|
code ??= diagnostic.code;
|
|
50
54
|
if (diagnostic.name === "SuppressedError")
|
|
51
55
|
pending.push(diagnostic.suppressed, diagnostic.error);
|
|
@@ -55,15 +59,32 @@ function descriptorExhaustion(error) {
|
|
|
55
59
|
}
|
|
56
60
|
if (!code)
|
|
57
61
|
return;
|
|
62
|
+
// Disk-space diagnostics retain the published direct-write code and already-classified errors.
|
|
63
|
+
if (code === "ENOSPC" && error instanceof FsSafeError)
|
|
64
|
+
return;
|
|
58
65
|
const cleanup = details?.cleanup;
|
|
59
66
|
const publication = details?.publication;
|
|
60
67
|
const preserved = cleanup?.status === "preserved" && publication?.status === "indeterminate";
|
|
61
|
-
|
|
68
|
+
const message = code === "ENOSPC" ? PINNED_WRITE_ERRNO_MESSAGES.get(code) : "filesystem write failed: too many open files";
|
|
69
|
+
return new FsSafeError(code === "ENOSPC" ? "invalid-path" : "helper-failed", `${message} (${code})${preserved ? "; publication outcome is indeterminate; staged file preserved" : ""}`, { cause: error, details });
|
|
62
70
|
}
|
|
63
71
|
export function normalizePinnedWriteError(error) {
|
|
64
|
-
const exhausted =
|
|
72
|
+
const exhausted = resourceExhaustion(error);
|
|
65
73
|
if (exhausted)
|
|
66
74
|
return exhausted;
|
|
75
|
+
// Disposal retains cleanup evidence, but the failed publication remains primary.
|
|
76
|
+
let primary = error;
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
try {
|
|
79
|
+
while (primary instanceof Error && primary.name === "SuppressedError" && !seen.has(primary)) {
|
|
80
|
+
seen.add(primary);
|
|
81
|
+
primary = primary.suppressed;
|
|
82
|
+
}
|
|
83
|
+
if (primary !== error && primary instanceof FsSafeError && primary.details?.phase === "publish") {
|
|
84
|
+
return new FsSafeError(primary.code, primary.message, { cause: error, details: primary.details });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch { /* Unreadable diagnostics retain the original failure. */ }
|
|
67
88
|
if (error instanceof FsSafeError) {
|
|
68
89
|
return error;
|
|
69
90
|
}
|
|
@@ -1167,10 +1167,10 @@ async function movePathFallback(root, params) {
|
|
|
1167
1167
|
let sourceIdentity;
|
|
1168
1168
|
try {
|
|
1169
1169
|
if (params.overwrite && params.assertBeforeMutation) {
|
|
1170
|
-
sourceIdentity = inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true })
|
|
1170
|
+
sourceIdentity = inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true })));
|
|
1171
1171
|
}
|
|
1172
1172
|
else {
|
|
1173
|
-
admitMoveSourceStat(fsSync.lstatSync(source.resolved)
|
|
1173
|
+
admitMoveSourceStat(fsSync.lstatSync(source.resolved));
|
|
1174
1174
|
}
|
|
1175
1175
|
}
|
|
1176
1176
|
catch (error) {
|
|
@@ -1234,7 +1234,7 @@ async function movePathFallback(root, params) {
|
|
|
1234
1234
|
}
|
|
1235
1235
|
}
|
|
1236
1236
|
}
|
|
1237
|
-
inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true })
|
|
1237
|
+
inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true })), sourceIdentity);
|
|
1238
1238
|
}
|
|
1239
1239
|
await fs.rename(source.resolved, target.resolved);
|
|
1240
1240
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
2
|
import type { RootMoveOptions } from "./root-options.js";
|
|
3
3
|
import { type RootContext } from "./root-context.js";
|
|
4
|
-
export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T
|
|
5
|
-
export declare function movePathNative(root: RootContext, params: Pick<RootMoveOptions, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"
|
|
4
|
+
export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T): T;
|
|
5
|
+
export declare function movePathNative(root: RootContext, params: Pick<RootMoveOptions, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
|
|
6
|
+
requireNative: boolean;
|
|
7
|
+
}, paths: {
|
|
6
8
|
sourcePath: string;
|
|
7
9
|
sourceParentPath: string;
|
|
8
10
|
targetPath: string;
|
|
@@ -15,6 +15,7 @@ import { PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
|
15
15
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
16
16
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
17
17
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
18
|
+
import { nativeMoveNoReplace } from "./native-move-fallback.js";
|
|
18
19
|
function nativeParentRelativePath(rootReal, parentPath) {
|
|
19
20
|
const relative = path.relative(rootReal, parentPath);
|
|
20
21
|
if (relative === "" || relative === ".")
|
|
@@ -57,22 +58,13 @@ function normalizeMoveError(error) {
|
|
|
57
58
|
}
|
|
58
59
|
return error;
|
|
59
60
|
}
|
|
60
|
-
function
|
|
61
|
-
if (error?.code === "EINVAL") {
|
|
62
|
-
return new FsSafeError("helper-unavailable", "native no-replace move is unavailable on this filesystem", errorCauseOptions(error));
|
|
63
|
-
}
|
|
64
|
-
return normalizeMoveError(error);
|
|
65
|
-
}
|
|
66
|
-
export function admitMoveSourceStat(stat, overwrite = false) {
|
|
61
|
+
export function admitMoveSourceStat(stat) {
|
|
67
62
|
if (stat.isSymbolicLink()) {
|
|
68
63
|
throw new FsSafeError("symlink", "symlink not allowed");
|
|
69
64
|
}
|
|
70
65
|
if (stat.isFile() && stat.nlink > 1) {
|
|
71
66
|
throw hardlinkedPathNotAllowedError();
|
|
72
67
|
}
|
|
73
|
-
if (!overwrite && stat.isDirectory()) {
|
|
74
|
-
throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
75
|
-
}
|
|
76
68
|
return stat;
|
|
77
69
|
}
|
|
78
70
|
export async function movePathNative(root, params, paths, overwrite = false) {
|
|
@@ -124,10 +116,9 @@ export async function movePathNative(root, params, paths, overwrite = false) {
|
|
|
124
116
|
await assertRootIdentityCurrent(root);
|
|
125
117
|
for (const admission of parentAdmissions)
|
|
126
118
|
assertSyncDirectoryGuard(admission.guard);
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
? inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true }), overwrite), paths.expectedSourceIdentity)
|
|
119
|
+
// Linux auto fallback needs an exact source even without an authority callback.
|
|
120
|
+
const sourceStat = overwrite || params.assertBeforeMutation || (process.platform === "linux" && !params.requireNative)
|
|
121
|
+
? inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })), paths.expectedSourceIdentity)
|
|
131
122
|
: undefined;
|
|
132
123
|
if (!sourceStat)
|
|
133
124
|
admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath));
|
|
@@ -172,20 +163,25 @@ export async function movePathNative(root, params, paths, overwrite = false) {
|
|
|
172
163
|
}
|
|
173
164
|
}
|
|
174
165
|
if (sourceStat !== undefined) {
|
|
175
|
-
inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })
|
|
166
|
+
inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })), sourceStat);
|
|
176
167
|
}
|
|
177
168
|
try {
|
|
178
169
|
assertFinalSymlinkRejected(admittedTargetPath, params.mutationSymlinks !== undefined);
|
|
179
170
|
if (overwrite)
|
|
180
171
|
binding.renameReplaceWithIdentity(sourceParent.fd, path.basename(paths.sourcePath), targetParent.fd, path.basename(paths.targetPath), sourceStat.dev, sourceStat.ino);
|
|
181
172
|
else
|
|
182
|
-
binding
|
|
173
|
+
nativeMoveNoReplace(binding, [
|
|
174
|
+
sourceParent.fd,
|
|
175
|
+
path.basename(paths.sourcePath),
|
|
176
|
+
targetParent.fd,
|
|
177
|
+
path.basename(paths.targetPath),
|
|
178
|
+
], sourceStat, !params.requireNative);
|
|
183
179
|
}
|
|
184
180
|
catch (error) {
|
|
185
181
|
if (isAlreadyExistsError(error) || (!overwrite && error?.code === "ENOTEMPTY")) {
|
|
186
182
|
throw new FsSafeError("already-exists", "destination exists", errorCauseOptions(error));
|
|
187
183
|
}
|
|
188
|
-
throw
|
|
184
|
+
throw normalizeMoveError(error);
|
|
189
185
|
}
|
|
190
186
|
try {
|
|
191
187
|
for (const admission of parentAdmissions)
|
|
@@ -8,6 +8,8 @@ import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
|
8
8
|
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
9
9
|
import { FsSafeError } from "./errors.js";
|
|
10
10
|
import { getNativeBinding } from "./native.js";
|
|
11
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
12
|
+
import { cachedNoReplaceDevice, isNoReplaceUnavailable } from "./native-noreplace.js";
|
|
11
13
|
import { handoffPrivateProducerFile, } from "./private-producer-handoff.js";
|
|
12
14
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
13
15
|
import { rootFromDirectoryGuard } from "./root-impl.js";
|
|
@@ -109,8 +111,18 @@ async function writeIsolatedProducer(params) {
|
|
|
109
111
|
const result = await Reflect.apply(write, writeReceiver, [producerPath]);
|
|
110
112
|
assertCurrent();
|
|
111
113
|
if (native) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
try {
|
|
115
|
+
const cached = cachedNoReplaceDevice(native, parentGuard.stat.dev, "producer handoff");
|
|
116
|
+
if (cached)
|
|
117
|
+
throw cached;
|
|
118
|
+
await targetRoot.move(path.relative(targetRoot.rootReal, producerPath), path.basename(tempPath), { assertBeforeMutation: assertCurrent });
|
|
119
|
+
return { cleanupWorkspace, result };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (isFsSafeNativeRequired() || !isNoReplaceUnavailable(error))
|
|
123
|
+
throw error;
|
|
124
|
+
assertCurrent();
|
|
125
|
+
}
|
|
114
126
|
}
|
|
115
127
|
const handoff = await handoffPrivateProducerFile({
|
|
116
128
|
sourcePath: producerPath,
|
|
@@ -126,7 +138,10 @@ async function writeIsolatedProducer(params) {
|
|
|
126
138
|
await cleanupWorkspace();
|
|
127
139
|
}
|
|
128
140
|
catch (cleanupError) {
|
|
129
|
-
|
|
141
|
+
const combined = new AggregateError([error, cleanupError], "isolated producer operation and workspace cleanup failed");
|
|
142
|
+
if (isNoReplaceUnavailable(error))
|
|
143
|
+
throw new FsSafeError(error.code, error.message, { cause: combined });
|
|
144
|
+
throw combined;
|
|
130
145
|
}
|
|
131
146
|
throw error;
|
|
132
147
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { noReplaceUnavailable } from "./native-noreplace.js";
|
|
2
3
|
export function createStagedFileReceipt(directory, temporaryBasename, stat) {
|
|
3
4
|
return Object.freeze({
|
|
4
5
|
directory,
|
|
@@ -11,6 +12,7 @@ export function createStagedFileReceipt(directory, temporaryBasename, stat) {
|
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
export function stagedFailure(kind, error, details) {
|
|
15
|
+
error = noReplaceUnavailable(error, `${kind} publication`) ?? error;
|
|
14
16
|
let code = "helper-failed";
|
|
15
17
|
try {
|
|
16
18
|
code = error instanceof FsSafeError ? error.code
|
|
@@ -19,7 +21,8 @@ export function stagedFailure(kind, error, details) {
|
|
|
19
21
|
catch {
|
|
20
22
|
// Uninspectable error metadata must not interrupt terminal settlement.
|
|
21
23
|
}
|
|
22
|
-
return new FsSafeError(code,
|
|
24
|
+
return new FsSafeError(code, code === "helper-unavailable" && error instanceof FsSafeError
|
|
25
|
+
? error.message : `staged ${kind} ${details.phase} failed`, { cause: error, details });
|
|
23
26
|
}
|
|
24
27
|
/** Finish owned cleanup and every close before surfacing publication evidence. */
|
|
25
28
|
export async function settleStagedFile(params) {
|
|
@@ -9,6 +9,7 @@ export declare class TempWorkspaceCleanupCapability {
|
|
|
9
9
|
readonly parent: RetainedDirectory | undefined;
|
|
10
10
|
constructor(root: string, safety: TempWorkspaceCleanupSafety, admission: TempWorkspaceRootAdmission, dirMode: number);
|
|
11
11
|
get canRemoveOwnedTree(): boolean;
|
|
12
|
+
get canUseCompatibleCleanup(): boolean;
|
|
12
13
|
prepareChildCreation(): void;
|
|
13
14
|
admitChildDescriptor(canEnumerate: boolean): boolean;
|
|
14
15
|
assertCurrent(): void;
|
|
@@ -8,6 +8,8 @@ import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
|
8
8
|
import { getNativeBinding } from "./native.js";
|
|
9
9
|
import { openTempWorkspaceCleanupParent, TempWorkspaceRetainedChild, } from "./temp-workspace-descriptor.js";
|
|
10
10
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
11
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
12
|
+
import { cachedNoReplaceUnavailable, noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
|
|
11
13
|
function isNativeCleanupBinding(binding) {
|
|
12
14
|
return typeof binding?.renameNoReplace === "function" &&
|
|
13
15
|
typeof binding.removeOwnedTree === "function" &&
|
|
@@ -94,6 +96,9 @@ export class TempWorkspaceCleanupCapability {
|
|
|
94
96
|
return (this.#phase === "ready" || this.#phase === "sealed") &&
|
|
95
97
|
this.#ownedTreeRemovalAvailable;
|
|
96
98
|
}
|
|
99
|
+
get canUseCompatibleCleanup() {
|
|
100
|
+
return this.#safety === "compatible" && !isFsSafeNativeRequired();
|
|
101
|
+
}
|
|
97
102
|
prepareChildCreation() {
|
|
98
103
|
const replay = this.#phase === "ready";
|
|
99
104
|
if (this.#phase !== "new" && !replay) {
|
|
@@ -269,11 +274,27 @@ export class TempWorkspaceCleanupOwner {
|
|
|
269
274
|
}
|
|
270
275
|
const name = `.fs-safe-workspace-cleanup-${randomUUID()}`;
|
|
271
276
|
const quarantinePath = path.join(parent.receipt.path, name);
|
|
272
|
-
|
|
277
|
+
let nativeRemoval = this.#capability.canRemoveOwnedTree && this.#directory !== undefined;
|
|
273
278
|
if (nativeRemoval) {
|
|
274
|
-
this.#capability.binding
|
|
279
|
+
const binding = this.#capability.binding;
|
|
280
|
+
try {
|
|
281
|
+
const cached = cachedNoReplaceUnavailable(binding, parent.fd, "workspace quarantine");
|
|
282
|
+
if (cached)
|
|
283
|
+
throw cached;
|
|
284
|
+
binding.renameNoReplace(parent.fd, path.basename(this.#dir), parent.fd, name);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
const unavailable = error instanceof FsSafeError && error.code === "helper-unavailable"
|
|
288
|
+
? error : noReplaceUnavailable(error, "workspace quarantine", true);
|
|
289
|
+
if (!unavailable)
|
|
290
|
+
throw error;
|
|
291
|
+
rememberNoReplaceUnavailable(binding, parent.fd, unavailable);
|
|
292
|
+
if (!this.#capability.canUseCompatibleCleanup)
|
|
293
|
+
throw unavailable;
|
|
294
|
+
nativeRemoval = false;
|
|
295
|
+
}
|
|
275
296
|
}
|
|
276
|
-
|
|
297
|
+
if (!nativeRemoval) {
|
|
277
298
|
// The admitted receipt is exact and descriptor-associated. Reuse it as
|
|
278
299
|
// the pre/post parent fence instead of layering a numeric guard over it.
|
|
279
300
|
this.#capability.assertCurrent();
|
|
@@ -288,7 +309,9 @@ export class TempWorkspaceCleanupOwner {
|
|
|
288
309
|
}
|
|
289
310
|
return { name, path: quarantinePath, nativeRemoval };
|
|
290
311
|
}
|
|
291
|
-
catch {
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (error instanceof FsSafeError && error.code === "helper-unavailable")
|
|
314
|
+
throw error;
|
|
292
315
|
// A failed rename can still have committed on a remote filesystem.
|
|
293
316
|
return "indeterminate";
|
|
294
317
|
}
|
|
@@ -82,6 +82,7 @@ concurrent namespace changes.
|
|
|
82
82
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
|
83
83
|
| `retainEntryForPublication`, `RetainedEntryPublication`, `RetainEntryForPublicationOptions`, `EntryPublicationReceipt`, `EntryPublicationResult`, `EntryPublicationIssue`, `PublicationIdentity`, `PublicationParent` | [One-way entry publication](entry-publication.md) | Native no-replace directory/regular-file/symlink export, including Windows NTFS junctions, under caller-exclusive namespace; explicit transition receipts, close-only disposal, no source CAS. |
|
|
84
84
|
| `retainFileInDirectory`, `RetainedFile`, related receipt/result types | [Retained Windows files](retained-file.md) | Existing-file native handle custody and explicit removal; local NTFS, producer authority required, no persistence guarantee. |
|
|
85
|
+
| `copyRootFileSync`, `CopyRootFileSyncOptions`, `CopiedRootFileSync` | [copy.md](copy.md#synchronous-guarded-file-copies) | Synchronous guarded exclusive file copy, optionally cloned and bound to an expected source identity, returning an owned destination descriptor and both identities. |
|
|
85
86
|
| `sameFileIdentity`, `FileIdentityStat` | – | Compare two stats for same-inode equality. |
|
|
86
87
|
| `readDirectoryIdentity`, `assertDirectoryIdentitySync`, `DirectoryIdentity` | [directory-identity.md](directory-identity.md) | Observe exact bigint directory identity and synchronously check a caller-selected path, optionally retaining its canonical path. |
|
|
87
88
|
| `pathExists`, `pathExistsSync` | – | Boolean existence check that does not throw on `ENOENT`. |
|
|
@@ -179,7 +179,8 @@ between native and JavaScript paths rather than reimplementing it in Rust.
|
|
|
179
179
|
|
|
180
180
|
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless internal separator and dot-component equivalence is allowed only after validation; every raw and Unicode interpretation must also agree on whether its name ends in `/` or `\`. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
181
181
|
|
|
182
|
-
ZIP preflight and bounded reads
|
|
182
|
+
ZIP preflight and bounded reads use extraction's canonical path identity, including
|
|
183
|
+
removing directory trailing separators, and apply its case-insensitive NFC collision
|
|
183
184
|
policy to the complete archive, including entries that extraction would strip
|
|
184
185
|
or skip. Known UTF-8 and Unicode Path names are checked during physical admission;
|
|
185
186
|
legacy names retain backend-selected decoding and undergo the same collision
|