mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(reference): join internal symlink targets with the inventory NFC normalizer (#1535)
* fix(reference): join internal symlink targets with the inventory NFC normalizer checkSymlinks resolved an internal symlink target with bare posix.normalize and looked the result up in the inventoried path set. A target that a filesystem reports in NFD (macOS readlink commonly does) joined to an NFD spelling while the inventory holds NFC, so validation reported "Internal symlink target must resolve to an inventoried entry" for a target the inventory actually contains. Route the join through normalizeJoinedLogicalPath, the same inventory identity normalizer #1513 applied to the runtime, HTML/module, and import joins from #1488. The feature-seed NFKC query fold stays a query. Refs #1488 * fix(reference): preserve source spelling during symlink lookup --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
905e4866c4
commit
0363e5a8e1
2 files changed
+68
-1
No files matched your search
@@ -409,3 +409,56 @@ describe("historical source graph validation", () => {
|
||||
).toThrow(/identifier does not match/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("historical source graph symlink path identity", () => {
|
||||
it("joins internal symlink targets with the inventory NFC normalizer", () => {
|
||||
// "café" spelled NFC, as the inventory produces it, and the same
|
||||
// name spelled NFD, as a macOS readlink can report it.
|
||||
const nfcPath = "src/café.ts";
|
||||
const nfdTarget = "cafe\u0301.ts";
|
||||
expect(nfcPath).not.toBe(nfdTarget);
|
||||
|
||||
const input = { ...graphInput(), inventory_state: "partial" as const };
|
||||
input.entries[1] = fileEntry({ path: nfcPath });
|
||||
input.entries[2] = symlinkEntry({
|
||||
path: "src/link.js",
|
||||
target: nfdTarget,
|
||||
target_state: "internal",
|
||||
});
|
||||
input.relationships = [];
|
||||
const graph = createHistoricalSourceGraph(input);
|
||||
expect(graph.entries[2]).toMatchObject({
|
||||
kind: "symlink",
|
||||
target: nfdTarget,
|
||||
target_state: "internal",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an exact NFD inventory spelling when resolving its symlink", () => {
|
||||
const input = { ...graphInput(), inventory_state: "partial" as const };
|
||||
input.entries[1] = fileEntry({ path: "src/cafe\u0301.ts" });
|
||||
input.entries[2] = symlinkEntry({
|
||||
path: "src/link.js",
|
||||
target: "cafe\u0301.ts",
|
||||
target_state: "internal",
|
||||
});
|
||||
input.relationships = [];
|
||||
expect(createHistoricalSourceGraph(input).entries[1]?.path).toBe(
|
||||
"src/cafe\u0301.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("still rejects an internal symlink target that no inventory holds", () => {
|
||||
const input = { ...graphInput(), inventory_state: "partial" as const };
|
||||
input.entries[2] = symlinkEntry({
|
||||
path: "src/link.js",
|
||||
// The NFD spelling normalizes to "src/café.ts", which the
|
||||
// inventory does not contain, so the join must still miss.
|
||||
target: "cafe\u0301.ts",
|
||||
target_state: "internal",
|
||||
});
|
||||
expect(() => createHistoricalSourceGraph(input)).toThrow(
|
||||
/must resolve to an inventoried entry/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
|
||||
import { canonicalJson } from "./comparisonSemantics.js";
|
||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
import { normalizeJoinedLogicalPath } from "./artifactIdentity.js";
|
||||
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
@@ -313,6 +314,14 @@ const checkSymlinks = (
|
||||
paths: ReadonlySet<string>,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
const normalizedPathCounts = new Map<string, number>();
|
||||
for (const path of paths) {
|
||||
const identity = normalizeJoinedLogicalPath(path);
|
||||
normalizedPathCounts.set(
|
||||
identity,
|
||||
(normalizedPathCounts.get(identity) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
for (const [index, entry] of graph.entries.entries()) {
|
||||
if (entry.kind !== "symlink") continue;
|
||||
if (entry.target === null) continue;
|
||||
@@ -330,7 +339,12 @@ const checkSymlinks = (
|
||||
const resolved = posix.normalize(
|
||||
posix.join(posix.dirname(entry.path), entry.target),
|
||||
);
|
||||
if (resolved.startsWith("../") || resolved === ".." || !paths.has(resolved))
|
||||
const normalized = normalizeJoinedLogicalPath(resolved);
|
||||
if (
|
||||
resolved.startsWith("../") ||
|
||||
resolved === ".." ||
|
||||
(!paths.has(resolved) && normalizedPathCounts.get(normalized) !== 1)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Internal symlink target must resolve to an inventoried entry",
|
||||
|
||||
Reference in new issue
Block a user