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:
morluto committed 2026-10-24 00:11:41 +08:00
1 parent 51e5938776
commit 7ba05210fe
3 files changed
+81 -4

No files matched your search

+20 -3
View File
@@ -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",