mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 11:25:39 +00:00
fix(reference): preserve observed directories and unreadable links
Continue validating child directories observed before a parent enumeration failure. Reconcile unobserved link targets at the reader using canonical path identity and the nearest failed ancestor, preserving exclusions and cancellation. Refs: #4013, #4053, #4060 Co-authored-by: XY-code1 <1611761391@qq.com>
This commit is contained in:
1 parent
ee250816e1
commit
d6e4fe4a4d
6 files changed
+401
-4
No files matched your search
@@ -1,10 +1,14 @@
|
||||
import { posix } from "node:path";
|
||||
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
import { createReferenceSourcePathLookup } from "../domain/referenceSourcePathIdentity.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { traverseDirectory } from "./ReferenceSourceReaderEntries.js";
|
||||
import type {
|
||||
ReferenceSourceReaderOptions,
|
||||
ReferenceSourceEntry,
|
||||
ReferenceSourceRead,
|
||||
ReferenceSourceResult,
|
||||
TraversalState,
|
||||
@@ -61,10 +65,20 @@ const readSource = async (
|
||||
? {}
|
||||
: { shouldExclude: options.shouldExclude }),
|
||||
entries: [],
|
||||
exclusions: [],
|
||||
pending: [{ path: canonicalRoot }],
|
||||
bytesRead: 0,
|
||||
};
|
||||
const traversal = await traverse(state);
|
||||
if (
|
||||
!isAborted(state.signal) &&
|
||||
(traversal.ok || traversal.error.code !== "cancelled")
|
||||
)
|
||||
reconcileUnobservedSymlinkTargets(
|
||||
state.entries,
|
||||
state.exclusions,
|
||||
state.signal,
|
||||
);
|
||||
state.entries.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.path, right.path),
|
||||
);
|
||||
@@ -77,6 +91,96 @@ const readSource = async (
|
||||
return traversal.ok ? ok(read) : err({ ...traversal.error, partial: read });
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve internal link observations against the completed reader inventory.
|
||||
* If the closest containing directory failed before enumeration completed,
|
||||
* the target remains unknown; a broader failed ancestor cannot override a
|
||||
* successfully enumerated directory or a recorded exclusion.
|
||||
*/
|
||||
export const reconcileUnobservedSymlinkTargets = (
|
||||
entries: ReferenceSourceEntry[],
|
||||
exclusions: TraversalState["exclusions"] = [],
|
||||
signal?: AbortSignal,
|
||||
): void => {
|
||||
const failedDirectories = new Map(
|
||||
entries.flatMap((entry) =>
|
||||
entry.status === "failed" &&
|
||||
entry.kind === "directory" &&
|
||||
entry.code !== "cancelled"
|
||||
? [[entry.path, entry] as const]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
if (failedDirectories.size === 0 || isAborted(signal)) return;
|
||||
const directoryEntries = entries.filter(
|
||||
(entry) => entry.kind === "directory",
|
||||
);
|
||||
const directoryPaths = createReferenceSourcePathLookup(
|
||||
directoryEntries.map(({ path }) => path),
|
||||
);
|
||||
const observedPaths = createReferenceSourcePathLookup(
|
||||
entries.map(({ path }) => path),
|
||||
);
|
||||
const excludedPaths = createReferenceSourcePathLookup(
|
||||
exclusions.map(({ path }) => path),
|
||||
);
|
||||
for (const [index, entry] of entries.entries()) {
|
||||
if (isAborted(signal)) return;
|
||||
if (
|
||||
entry.status !== "read" ||
|
||||
entry.kind !== "symlink" ||
|
||||
entry.targetState !== "internal" ||
|
||||
observedPaths.resolve(entry.target) !== undefined ||
|
||||
excludedPaths.resolve(entry.target) !== undefined
|
||||
)
|
||||
continue;
|
||||
const ancestor = nearestFailedAncestor(
|
||||
entry.target,
|
||||
directoryPaths,
|
||||
failedDirectories,
|
||||
excludedPaths,
|
||||
signal,
|
||||
);
|
||||
if (ancestor === undefined) continue;
|
||||
entries[index] = {
|
||||
status: "failed",
|
||||
kind: "symlink",
|
||||
path: entry.path,
|
||||
code: ancestor.code,
|
||||
message: `Symbolic link target is inside a directory that could not be enumerated: ${ancestor.message}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const nearestFailedAncestor = (
|
||||
target: string,
|
||||
directoryPaths: ReturnType<typeof createReferenceSourcePathLookup>,
|
||||
failedDirectories: ReadonlyMap<
|
||||
string,
|
||||
Extract<ReferenceSourceEntry, { status: "failed" }>
|
||||
>,
|
||||
excludedPaths: ReturnType<typeof createReferenceSourcePathLookup>,
|
||||
signal: AbortSignal | undefined,
|
||||
): Extract<ReferenceSourceEntry, { status: "failed" }> | undefined => {
|
||||
for (
|
||||
let candidate = posix.dirname(target);
|
||||
candidate !== "." && candidate !== "/";
|
||||
candidate = posix.dirname(candidate)
|
||||
) {
|
||||
if (isAborted(signal)) return undefined;
|
||||
if (excludedPaths.resolve(candidate) !== undefined) return undefined;
|
||||
const observedDirectory = directoryPaths.resolve(candidate);
|
||||
if (observedDirectory !== undefined)
|
||||
return failedDirectories.get(observedDirectory);
|
||||
}
|
||||
if (isAborted(signal)) return undefined;
|
||||
if (excludedPaths.resolve(".") !== undefined) return undefined;
|
||||
const observedRoot = directoryPaths.resolve(".");
|
||||
return observedRoot === undefined
|
||||
? undefined
|
||||
: failedDirectories.get(observedRoot);
|
||||
};
|
||||
|
||||
const traverse = async (
|
||||
state: TraversalState,
|
||||
): Promise<ReferenceSourceResult<undefined>> => {
|
||||
|
||||
Reference in new issue
Block a user