mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(reference): preserve raw symlink target identity
Read link targets as bytes before strict UTF-8 admission. Retain malformed targets as unreadable with source hex; preserve literal replacement characters and valid BOM-prefixed targets. Fixes #6765
This commit is contained in:
+4
-1
@@ -426,7 +426,10 @@ top-level limitation. That partial inventory does not establish absence among
|
||||
unexamined children. POSIX directory-entry bytes that are not valid UTF-8 cannot
|
||||
be represented as graph paths; the containing directory (or root) is marked
|
||||
unsupported, and REA does not access the entry through a replacement-character
|
||||
name. Readable siblings remain inventoried.
|
||||
name. Symlink targets are read as bytes too: a target that is not valid UTF-8
|
||||
is retained as an unreadable symlink with its bytes reported in the limitation,
|
||||
and is never resolved through a replacement-character path. Readable siblings
|
||||
remain inventoried.
|
||||
|
||||
JavaScript and TypeScript import parsing requires valid UTF-8. Malformed source
|
||||
bytes retain their original hashes and sizes with a decoding diagnostic; REA
|
||||
|
||||
@@ -377,9 +377,12 @@ const readSymlinkTarget = async (
|
||||
absolute: string,
|
||||
path: string,
|
||||
): Promise<ReferenceSourceEntry> => {
|
||||
let rawTarget: string;
|
||||
let rawTarget: Buffer;
|
||||
try {
|
||||
rawTarget = await readlink(absolute);
|
||||
// Keep the symlink's source bytes until UTF-8 validity is known. The
|
||||
// default string encoding can alias an invalid target with a real U+FFFD
|
||||
// sibling during the subsequent path lookup.
|
||||
rawTarget = await readlink(absolute, { encoding: "buffer" });
|
||||
} catch (cause: unknown) {
|
||||
// POSIX readlink reports EINVAL when the observed link became a non-link.
|
||||
// Keep this classification at the syscall, rather than hiding EINVAL from
|
||||
@@ -388,7 +391,21 @@ const readSymlinkTarget = async (
|
||||
return changedSymlink(path);
|
||||
throw cause;
|
||||
}
|
||||
const lexicalTarget = resolve(dirname(absolute), rawTarget);
|
||||
let target: string;
|
||||
try {
|
||||
target = new TextDecoder("utf-8", {
|
||||
fatal: true,
|
||||
ignoreBOM: true,
|
||||
}).decode(rawTarget);
|
||||
} catch {
|
||||
return entryFailure(
|
||||
path,
|
||||
"symlink",
|
||||
"unsupported",
|
||||
`Symlink target is not valid UTF-8 (raw target bytes: ${rawTarget.toString("hex")})`,
|
||||
);
|
||||
}
|
||||
const lexicalTarget = resolve(dirname(absolute), target);
|
||||
if (!isPathWithinRoot(root, lexicalTarget))
|
||||
return {
|
||||
status: "read",
|
||||
|
||||
@@ -133,6 +133,63 @@ cliTest.skipIf(process.platform !== "linux")(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.skipIf(process.platform === "win32")(
|
||||
"keeps raw symlink target identities distinct from UTF-8 replacement and BOM handling",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-reference-link-bytes-");
|
||||
const config = await createTestTempDirectory(
|
||||
"rea-reference-link-bytes-config-",
|
||||
);
|
||||
const rawTarget = Buffer.from([0x62, 0x61, 0x64, 0xff, 0x2e, 0x74, 0x73]);
|
||||
await Promise.all([
|
||||
writeFile(join(root, "bad\ufffd.ts"), "export const unicode = 1;\n"),
|
||||
symlink(rawTarget, join(root, "alias.ts")),
|
||||
writeFile(join(root, "\ufefftarget.ts"), "export const bom = 1;\n"),
|
||||
symlink("\ufefftarget.ts", join(root, "bom-alias.ts")),
|
||||
]);
|
||||
|
||||
const result = await cli.run({
|
||||
arguments: ["import-reference-source", root, "--json"],
|
||||
environment: {
|
||||
HOME: config,
|
||||
XDG_CONFIG_HOME: config,
|
||||
XDG_CACHE_HOME: config,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode, result.stderr + JSON.stringify(result.json)).toBe(
|
||||
0,
|
||||
);
|
||||
const graph = historicalSourceGraphSchema.parse(result.json);
|
||||
expect(graph.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "symlink",
|
||||
path: "alias.ts",
|
||||
target: null,
|
||||
target_state: "unreadable",
|
||||
limitations: [expect.stringContaining(rawTarget.toString("hex"))],
|
||||
}),
|
||||
);
|
||||
expect(graph.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "file",
|
||||
path: "bad\ufffd.ts",
|
||||
content_state: "hashed",
|
||||
}),
|
||||
);
|
||||
expect(graph.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "symlink",
|
||||
path: "bom-alias.ts",
|
||||
target: "\ufefftarget.ts",
|
||||
target_state: "internal",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
await readlink(join(root, "alias.ts"), { encoding: "buffer" }),
|
||||
).toEqual(rawTarget);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.skipIf(process.platform === "win32")(
|
||||
"preserves historical literal-backslash entries and their exact owning import base",
|
||||
async ({ cli }) => {
|
||||
|
||||
Reference in New Issue
Block a user