fix(ghidra): read UTF-8 signed language definitions (#2772)

Buffer#toString kept a leading UTF-8 byte order mark as U+FEFF, so the
strict XML parser rejected a valid .ldefs file as content outside the root
element and aborted the whole language catalog read. XML 1.0 section 4.3.3
defines the mark as an encoding signature; decode with TextDecoder, which
consumes it. Malformed XML is still rejected.

Fixes #2533

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yasshmittal 2026-10-13 18:11:28 +05:30 committed by GitHub
parent 8823c55b0c
commit ec4f4ddc58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 26 additions and 1 deletions

View File

@ -75,8 +75,10 @@ export const readGhidraLanguageCatalog = async (
if (!entry.endsWith(".ldefs")) continue;
const bytes = await readFile(join(directory, entry));
if (bytes.length > MAX_LDEFS_BYTES) continue;
// XML 1.0 section 4.3.3: a leading UTF-8 BOM is an encoding signature,
// not document content; TextDecoder consumes it, Buffer#toString does not.
definitions.push(
...parseGhidraLanguageDefinitions(bytes.toString("utf8")),
...parseGhidraLanguageDefinitions(new TextDecoder().decode(bytes)),
);
}
}

View File

@ -2,6 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { parseConfig } from "../config/parseConfig.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
@ -190,6 +191,28 @@ describe("Ghidra language catalog", () => {
);
});
it("reads a UTF-8 signed definition beside an unsigned sibling", async () => {
const root = await createTestTempDirectory("rea-ghidra-bom-");
const module = (name: string) =>
join(root, "Ghidra/Processors", name, "data/languages");
await mkdir(module("probe"), { recursive: true });
await mkdir(module("x86"), { recursive: true });
await writeFile(
join(module("probe"), "probe.ldefs"),
Buffer.concat([
Buffer.from([0xef, 0xbb, 0xbf]),
Buffer.from(
'<?xml version="1.0" encoding="UTF-8"?>\n<language_definitions><language id="probe:LE:32:default"/></language_definitions>',
),
]),
);
await writeFile(join(module("x86"), "x86.ldefs"), X86_LDEFS);
expect(await readGhidraLanguageCatalog(root)).toEqual([
{ id: "probe:LE:32:default", compilerSpecIds: [] },
...parseGhidraLanguageDefinitions(X86_LDEFS),
]);
});
it("admits only languages and compiler specs the installation defines", () => {
const catalog = parseGhidraLanguageDefinitions(X86_LDEFS);
expect(