test(ghidra): align MIPS verifier ABI and snapshot expectations

Fixes #3415. Retain independently inspected GNU attribute facts and compare exact snapshot wire fields. Preserve strict metadata comparisons, specific incomplete-ABI refusals and source immutability checks.

Original implementation by @AbdulSamad94.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
Abdul Samad Siddiqui
2026-10-15 19:00:16 +05:00
committed by GitHub
parent e9a71efa31
commit a40ee741c2
7 changed files with 437 additions and 121 deletions
+49 -84
View File
@@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import {
inspectMipsReadelf,
expectedMipsElfProfile,
inspectLegacyMipsReadelf,
assertMipsLoadedImage,
assertMipsProbe,
@@ -120,6 +121,17 @@ async function verifySoftFloat(call, symbols, byteOrder) {
);
return { storage: boundaries };
}
function assertIncompleteMipsInspection(error) {
assert.equal(error.code, "capability_unavailable");
assert.equal(error.details.selection_reason, "target_unsupported");
const rejection = error.details.rejections.find(
(candidate) => candidate.provider_id === "ghidra",
);
assert.equal(rejection?.code, "architecture_unsupported");
assert.match(rejection.reason, /ABI declaration inspection is incomplete/u);
assert.match(rejection.reason, /section header table/u);
}
const reports = [];
let primaryFailure;
try {
@@ -475,28 +487,9 @@ try {
JSON.parse(await readFile(snapshotPath, "utf8")),
);
assert.equal(snapshot.target.architecture, "mips");
const abi = independent.mips.abiFlags;
assert.deepEqual(
snapshot.binding.analysis_profile.parameters.mips_elf,
{
elf_class: independent.mips.elfClass,
byte_order: independent.mips.byteOrder,
type: independent.mips.type,
flags: independent.mips.flags,
abi_flags: {
version: abi.version,
isa_level: abi.isaLevel,
isa_revision: abi.isaRevision,
gpr_size: abi.gprSize,
cpr1_size: abi.cpr1Size,
cpr2_size: abi.cpr2Size,
fp_abi: abi.fpAbi,
isa_extension: abi.isaExtension,
ases: abi.ases,
flags1: abi.flags1,
flags2: abi.flags2,
},
},
expectedMipsElfProfile(independent.mips),
"Snapshot profile did not retain the independently inspected ELF/ABI declaration",
);
assert.equal(
@@ -617,7 +610,8 @@ try {
// PSP keeps its independent Allegrex/A-B-A checks; carved PSP is not verified.
let carved;
if (!psp) {
// Carved firmware keeps all program headers/load bytes but loses sections.
// A program-header ABI record survives carving, but GNU declarations in
// uncaptured section headers remain unknown: both adapters must refuse.
const read16 = (offset) =>
byteOrder === "little"
? sourceBytes.readUInt16LE(offset)
@@ -655,79 +649,50 @@ try {
carvedResolved.value.mips.limitations.join(" "),
/section header table/u,
);
const carvedTarget = await call("open_binary", {
path: carvedPath,
provider_id: "ghidra",
});
assert.equal(carvedTarget.sha256, carvedSha256);
const carvedImage = await call("inspect_native_load_image");
assertMipsLoadedImage(
carvedImage.observations,
byteOrder,
carvedSha256,
);
const carvedProcedures = await call("list_procedures");
assert.ok(
carvedProcedures.some(
(p) =>
BigInt(p.address) === BigInt(independent.symbols.rea_mips_entry),
const carvedError = requireMcpToolError(
await client.callTool(
{
name: "open_binary",
arguments: { path: carvedPath, provider_id: "ghidra" },
},
{ timeout: 60000 },
),
);
const carvedDossier = functionDossierSchema.parse(
await call("analyze_function", {
procedure: independent.symbols.rea_mips_entry,
}),
);
assert.ok(carvedDossier.pseudocode.trim().length > 0);
const carvedSnapshotPath = join(
workspace,
`${byteOrder}-${floatAbi}-carved.snapshot.json`,
);
await call("close_binary", { snapshot_path: carvedSnapshotPath });
const carvedSnapshot = parseAnalysisSnapshot(
JSON.parse(await readFile(carvedSnapshotPath, "utf8")),
);
assert.equal(carvedSnapshot.target.sha256, carvedSha256);
assertIncompleteMipsInspection(carvedError);
let carvedCliError;
try {
await execute(
process.execPath,
[
entrypoint,
"function",
carvedPath,
independent.symbols.rea_mips_entry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 60000, maxBuffer: 1024 * 1024 },
);
} catch (cause) {
carvedCliError = cause;
}
assert.ok(
carvedSnapshot.evidence_bundle.records.some((record) =>
parseEvidence(record).limitations.some((item) =>
item.includes("section header table"),
),
),
);
const carvedCli = await execute(
process.execPath,
[
entrypoint,
"function",
carvedPath,
independent.symbols.rea_mips_entry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const carvedEvidence = parseEvidence(JSON.parse(carvedCli.stdout));
assert.equal(carvedEvidence.subject?.digest.sha256, carvedSha256);
assert.ok(
carvedEvidence.limitations.some((item) =>
item.includes("section header table"),
),
);
assert.equal(
functionDossierSchema.parse(carvedEvidence.normalized_result)
.procedure.address,
carvedDossier.procedure.address,
carvedCliError &&
Number.isInteger(carvedCliError.code) &&
carvedCliError.code !== 0,
"Expected an incomplete-ABI refusal, not a timeout or success",
);
assertIncompleteMipsInspection(JSON.parse(carvedCliError.stdout));
assert.deepEqual(await readFile(carvedPath), carvedBytes);
carved = {
sha256: carvedSha256,
source_bytes: carvedBytes.length,
section_header_offset: sectionOffset,
effective_language: carvedImage.observations.language_id,
status: "expected-incomplete-abi-refusal",
limitations: carvedResolved.value.mips.limitations,
procedures: carvedProcedures.length,
cli_exit: carvedCliError.code,
mcp_error: carvedError,
};
}
reports.push({
+17 -2
View File
@@ -1,10 +1,25 @@
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
import type {
MipsAbiFlags,
MipsElfMetadata,
MipsGnuAttributes,
} from "../../../src/domain/binaryTargetTypes.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
export type MipsReadelfMetadata = MipsElfMetadata & {
readonly abiFlags: MipsAbiFlags;
readonly gnuAttributes: MipsGnuAttributes | null;
};
export function expectedMipsElfProfile(
mips: MipsReadelfMetadata,
): Readonly<Record<string, JsonValue>>;
export type MipsFloatAbi = "hard" | "soft";
export interface MipsReadelfFacts {
mips: MipsElfMetadata;
mips: MipsReadelfMetadata & { readonly gnuAttributes: null };
symbols: Record<string, string>;
}
export function parseMipsReadelfSectionTypes(text: string): readonly string[];
export function parseMipsReadelf(
text: string,
byteOrder: string,
+65 -1
View File
@@ -23,6 +23,65 @@ const SOFT_FLOAT_SYMBOLS = [
"rea_mips_soft_caller",
];
/** Complete indexed ELF32 section types shared by the independent fixture readers. */
export function parseMipsReadelfSectionTypes(text) {
const counts = [
...text.matchAll(/^\s*Number of section headers:\s*(\d+)\s*$/gmu),
];
assert.equal(counts.length, 1, "Missing or ambiguous readelf section count");
const sectionCount = Number(counts[0][1]);
assert.ok(sectionCount > 0, "Fixture must have a section header table");
const sections = [...text.matchAll(/^[ \t]*\[[ \t]*(\d+)\][ \t]+(.+)$/gmu)];
assert.equal(
sections.length,
sectionCount,
"Incomplete readelf section table",
);
return sections.map((section, index) => {
assert.equal(Number(section[1]), index, "Ambiguous readelf section index");
const columns =
/^(?:\S+[ \t]+)?(\S+)[ \t]+[\da-f]{8}[ \t]+[\da-f]+[ \t]+[\da-f]+[ \t]+[\da-f]{2}[ \t]+(?:[A-Za-z]+[ \t]+)?\d+[ \t]+\d+[ \t]+\d+[ \t]*$/u.exec(
section[2],
);
assert.ok(columns !== null, "Malformed readelf section header");
return columns[1];
});
}
/** Independent wire-format expectation shared by generic and PSP snapshots. */
export function expectedMipsElfProfile(mips) {
const abi = mips.abiFlags;
assert.ok(abi != null, "Fixture ABI declarations must be inspected");
assert.notEqual(
mips.gnuAttributes,
undefined,
"Fixture GNU attributes must be inspected",
);
return {
elf_class: mips.elfClass,
byte_order: mips.byteOrder,
type: mips.type,
flags: mips.flags,
gnu_attributes:
mips.gnuAttributes === null
? null
: { fp_abi: mips.gnuAttributes.fpAbi, msa: mips.gnuAttributes.msa },
abi_flags: {
version: abi.version,
isa_level: abi.isaLevel,
isa_revision: abi.isaRevision,
gpr_size: abi.gprSize,
cpr1_size: abi.cpr1Size,
cpr2_size: abi.cpr2Size,
fp_abi: abi.fpAbi,
isa_extension: abi.isaExtension,
ases: abi.ases,
flags1: abi.flags1,
flags2: abi.flags2,
},
};
}
/**
* Parse only the documented GNU readelf fields needed by our tiny fixtures.
* `rel` objects carry their own symbol checks in mips-relocatable-checks.mjs.
@@ -51,6 +110,10 @@ export function parseMipsReadelf(
objectType === "rel" ? /^REL\b/u : /^EXEC\b/u,
);
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^MIPS\b/u);
assert.ok(
!parseMipsReadelfSectionTypes(text).includes("GNU_ATTRIBUTES"),
"Fixture unexpectedly declares GNU attributes",
);
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+),.*$/gmu, "flags"));
assert.equal(flags, 0x70001001, "Fixture ELF flags differ from build intent");
assert.equal(field(/^ISA:\s*(.+)$/gmu, "ISA"), "MIPS32r2");
@@ -123,6 +186,7 @@ export function parseMipsReadelf(
type: objectType === "rel" ? 1 : 2,
flags,
abiFlags,
gnuAttributes: null,
},
symbols,
};
@@ -144,7 +208,7 @@ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
);
const result = await execute(
command,
["-h", "-A", "-s", "-W", path],
["-h", "-A", "-S", "-s", "-W", path],
options,
);
assert.equal(
+106 -2
View File
@@ -7,7 +7,7 @@ import {
parseMipsReadelf,
} from "./mips-checks.mjs";
// GNU readelf 2.40 output for the source-owned Clang 17 / LLD fixture.
// GNU readelf 2.44 output for the source-owned Clang 19 / LLD 19 fixture.
// This tests the reader boundary, not Ghidra; the real lane rebuilds both orders.
const readelf = `ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
@@ -21,7 +21,7 @@ const readelf = `ELF Header:
Version: 0x1
Entry point address: 0x201b0
Start of program headers: 52 (bytes into file)
Start of section headers: 2996 (bytes into file)
Start of section headers: 2624 (bytes into file)
Flags: 0x70001001, noreorder, o32, mips32r2
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
@@ -30,6 +30,38 @@ const readelf = `ELF Header:
Number of section headers: 24
Section header string table index: 22
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 00010118 000118 000018 18 A 0 0 8
[ 2] .reginfo MIPS_REGINFO 00010130 000130 000018 18 A 0 0 4
[ 3] .rodata PROGBITS 00010148 000148 00001e 00 A 0 0 1
[ 4] .text PROGBITS 00020170 000170 0000f0 00 AX 0 0 16
[ 5] .data PROGBITS 00030260 000260 000000 00 WA 0 0 16
[ 6] .got PROGBITS 00030260 000260 000008 00 WAp 0 0 16
[ 7] .sdata PROGBITS 00030268 000268 000004 00 WAp 0 0 4
[ 8] .bss NOBITS 00030270 00026c 000000 00 WA 0 0 16
[ 9] .mdebug.abi32 PROGBITS 00000000 00026c 000000 00 0 0 1
[10] .pdr PROGBITS 00000000 00026c 000060 00 0 0 4
[11] .debug_abbrev MIPS_DWARF 00000000 0002cc 0000b3 00 0 0 1
[12] .debug_info MIPS_DWARF 00000000 00037f 00015e 00 0 0 1
[13] .debug_str_offsets MIPS_DWARF 00000000 0004dd 000040 00 0 0 1
[14] .debug_str MIPS_DWARF 00000000 00051d 0000ef 01 MS 0 0 1
[15] .debug_addr MIPS_DWARF 00000000 00060c 000018 00 0 0 1
[16] .comment PROGBITS 00000000 000624 00003e 01 MS 0 0 1
[17] .debug_frame MIPS_DWARF 00000000 000664 00004c 00 0 0 4
[18] .debug_line MIPS_DWARF 00000000 0006b0 0000f8 00 0 0 1
[19] .debug_line_str MIPS_DWARF 00000000 0007a8 000098 01 MS 0 0 1
[20] .debug_aranges MIPS_DWARF 00000000 000840 000020 00 0 0 1
[21] .symtab SYMTAB 00000000 000860 000090 10 23 4 4
[22] .shstrtab STRTAB 00000000 0008f0 0000f0 00 0 0 1
[23] .strtab STRTAB 00000000 0009e0 00005f 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
@@ -54,6 +86,15 @@ ASEs:
None
FLAGS 1: 00000000
FLAGS 2: 00000000
Static GOT:
Canonical gp value: 00038250
Reserved entries:
Address Access Value
00030260 -32752(gp) 00000000
00030264 -32748(gp) 80000000
`;
const sha256 = "a".repeat(64);
const image = {
@@ -94,6 +135,7 @@ describe("MIPS real-verifier independent expectations", () => {
elfClass: 32,
byteOrder: "little",
flags: 0x70001001,
gnuAttributes: null,
abiFlags: {
isaLevel: 32,
isaRevision: 2,
@@ -229,6 +271,67 @@ describe("MIPS real-verifier independent expectations", () => {
});
});
describe("MIPS real-verifier GNU attribute coverage", () => {
it.each(["exec", "rel"] as const)(
"reports verified GNU attribute absence for both byte orders of %s fixtures",
(objectType) => {
for (const byteOrder of ["little", "big"]) {
const text = readelf
.replace("little endian", `${byteOrder} endian`)
.replace(
"EXEC (Executable file)",
objectType === "rel"
? "REL (Relocatable file)"
: "EXEC (Executable file)",
);
expect(
parseMipsReadelf(text, byteOrder, "hard", objectType).mips
.gnuAttributes,
).toBeNull();
}
},
);
it("rejects GNU attribute sections even when their name is unrelated", () => {
expect(() =>
parseMipsReadelf(
readelf.replace(
".comment PROGBITS",
".comment GNU_ATTRIBUTES",
),
"little",
),
).toThrow(/GNU attributes/u);
});
it("uses the section type rather than its name to establish absence", () => {
expect(
parseMipsReadelf(
readelf.replace(
".comment PROGBITS",
".gnu.attributes PROGBITS",
),
"little",
).mips.gnuAttributes,
).toBeNull();
});
it.each([
[
"missing table",
readelf.replace(
/Section Headers:[\s\S]*?Key to Flags:/u,
"Key to Flags:",
),
],
["missing entry", readelf.replace(/^.*\[ 5\].*\n/gmu, "")],
["duplicate index", readelf.replace("[ 5]", "[ 4]")],
["malformed entry", readelf.replace(/^.*\[ 5\].*$/gmu, " [ 5] malformed")],
])("refuses GNU attribute absence from a %s", (_name, text) => {
expect(() => parseMipsReadelf(text, "little")).toThrow(/section/u);
});
});
describe("MIPS soft-float real-verifier expectations", () => {
const softReadelf = readelf
.replace("CPR1 size: 32", "CPR1 size: 0")
@@ -249,6 +352,7 @@ describe("MIPS soft-float real-verifier expectations", () => {
it("reads the soft-float ABI record and its fixture symbols", () => {
const parsed = parseMipsReadelf(softReadelf, "little", "soft");
expect(parsed.mips.abiFlags).toMatchObject({ cpr1Size: 0, fpAbi: 3 });
expect(parsed.mips.gnuAttributes).toBeNull();
expect(parsed.symbols.rea_mips_soft_mixed).toBe("0x20380");
});
+142 -14
View File
@@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import { it } from "vitest";
import { parsePspReadelfAbi } from "./psp-fixture.mjs";
import { expectedMipsElfProfile } from "./mips-checks.mjs";
import { ghidraMipsProfileParameters } from "../../../src/ghidra/GhidraMipsProfile.js";
import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js";
import { parsePspReadelfMips } from "./psp-fixture.mjs";
// GNU readelf 2.44 output from the maintained PSPDEV v20261001 fixture.
const observed = `MIPS ABI Flags Version: 0
@@ -16,19 +19,42 @@ FLAGS 1: 00000001
FLAGS 2: 00000000
`;
// Constructed section-table controls combined with the captured ABI declaration;
// this does not establish a real PSPSDK or Allegrex-provider run.
const metadata = `ELF Header:
Class: ELF32
Data: 2's complement, little endian
Type: EXEC (Executable file)
Machine: MIPS R3000
Flags: 0x10a23001, noreorder, allegrex, eabi32, mips2
Number of section headers: 3
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 08800118 000118 000018 18 A 0 0 8
[ 2] .text PROGBITS 08800130 000130 000010 00 AX 0 0 16
${observed}`;
it("reads the independent PSP producer's single-float declaration", () => {
assert.deepEqual(parsePspReadelfAbi(observed), {
version: 0,
isaLevel: 2,
isaRevision: 0,
gprSize: 1,
cpr1Size: 1,
cpr2Size: 0,
fpAbi: 2,
isaExtension: 0,
ases: 0,
flags1: 1,
flags2: 0,
assert.deepEqual(parsePspReadelfMips(metadata), {
elfClass: 32,
byteOrder: "little",
type: 2,
flags: 0x10a23001,
gnuAttributes: null,
abiFlags: {
version: 0,
isaLevel: 2,
isaRevision: 0,
gprSize: 1,
cpr1Size: 1,
cpr2Size: 0,
fpAbi: 2,
isaExtension: 0,
ases: 0,
flags1: 1,
flags2: 0,
},
});
});
@@ -46,6 +72,108 @@ it.each([
])(
"rejects unsupported or ambiguous independent ABI output: %s",
(before, after) => {
assert.throws(() => parsePspReadelfAbi(observed.replace(before, after)));
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
},
);
it.each([
[
"missing table",
metadata.replace(
/Section Headers:[\s\S]*?MIPS ABI Flags/u,
"MIPS ABI Flags",
),
],
["incomplete table", metadata.replace(/^.*\[ 2\].*\n/gmu, "")],
["duplicate index", metadata.replace("[ 2]", "[ 1]")],
[
"GNU section under another name",
metadata.replace(
".text PROGBITS",
".text GNU_ATTRIBUTES",
),
],
])("refuses PSP GNU attribute absence from a %s", (_name, text) => {
assert.throws(() => parsePspReadelfMips(text), /section|GNU attributes/u);
});
it.each([
["Class: ELF32", "Class: ELF64"],
["little endian", "big endian"],
["Type: EXEC", "Type: REL"],
["Flags: 0x10a23001", "Flags: 0x70001001"],
["Class: ELF32", "Class: ELF32\n Class: ELF32"],
])("refuses contradictory or ambiguous PSP headers: %s", (before, after) => {
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
});
// GNU readelf 2.40 independently decoded this declaration from a constructed
// ELF32/PSP-shaped control, not a PSPSDK build or a real Allegrex import.
const gnuDeclaration = `Attribute Section: gnu
File Attributes
Tag_GNU_MIPS_ABI_FP: Hard float (single precision)
`;
const gnuMetadata = metadata.replace(
".text PROGBITS",
".text GNU_ATTRIBUTES",
);
it.each(["before", "after"])(
"retains the independent single-float GNU declaration %s the ABI dump",
(position) => {
const text =
position === "before"
? gnuMetadata.replace(observed, `${gnuDeclaration}\n${observed}`)
: `${gnuMetadata}\n${gnuDeclaration}`;
assert.deepEqual(parsePspReadelfMips(text), {
...parsePspReadelfMips(metadata),
gnuAttributes: { fpAbi: 2, msa: null },
});
},
);
it.each([
["unknown vendor", gnuDeclaration.replace("gnu", "aeabi")],
["wrong float ABI", gnuDeclaration.replace("single", "double")],
["unknown tag", `${gnuDeclaration} Tag_GNU_Unknown: 1\n`],
[
"unsupported scope",
gnuDeclaration.replace("File Attributes", "Section Attributes: 1"),
],
["duplicate declaration", `${gnuDeclaration}${gnuDeclaration}`],
])("refuses %s rather than inventing PSP GNU facts", (_name, declaration) => {
assert.throws(
() => parsePspReadelfMips(`${gnuMetadata}\n${declaration}`),
/GNU attributes/u,
);
});
it("refuses undeclared GNU attributes rather than reporting absence", () => {
assert.throws(
() => parsePspReadelfMips(`${metadata}\n${gnuDeclaration}`),
/GNU attributes/u,
);
});
it.each([
["absent", metadata],
["declared", `${gnuMetadata}\n${gnuDeclaration}`],
])(
"matches production snapshot serialization for %s GNU facts",
(_name, text) => {
const mips = parsePspReadelfMips(text);
const target = {
path: "/fixture/psp.elf",
sha256: "a".repeat(64),
kind: "executable",
format: "elf",
architecture: "mips",
availableArchitectures: ["mips"],
mips,
} satisfies BinaryTarget;
assert.deepEqual(
expectedMipsElfProfile(mips),
ghidraMipsProfileParameters(target).mips_elf,
);
},
);
+2 -2
View File
@@ -1,3 +1,3 @@
import type { MipsAbiFlags } from "../../../src/domain/binaryTargetTypes.js";
import type { MipsReadelfMetadata } from "./mips-checks.mjs";
export declare function parsePspReadelfAbi(text: string): MipsAbiFlags;
export declare function parsePspReadelfMips(text: string): MipsReadelfMetadata;
+56 -16
View File
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
import { copyFile, readFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { parseMipsReadelfSectionTypes } from "./mips-checks.mjs";
const execute = promisify(execFile);
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -42,15 +43,11 @@ export async function buildPspFixture(directory, globalValue = 7) {
assert.equal(bytes.readUInt32LE(36), 0x10a23001);
const readelf = await execute(
"psp-readelf",
["-h", "-A", "-sW", path],
["-h", "-A", "-S", "-sW", path],
options,
);
assert.equal(readelf.stderr.trim(), "");
assert.match(readelf.stdout, /Class:\s+ELF32/u);
assert.match(readelf.stdout, /Data:\s+2's complement, little endian/u);
assert.match(readelf.stdout, /Type:\s+EXEC\b/u);
assert.match(readelf.stdout, /Flags:\s+0x10a23001\b/u);
const abiFlags = parsePspReadelfAbi(readelf.stdout);
const mips = parsePspReadelfMips(readelf.stdout);
const abiDump = await execute(
"psp-readelf",
["-x", ".MIPS.abiflags", path],
@@ -99,13 +96,7 @@ export async function buildPspFixture(directory, globalValue = 7) {
source_sha256: sourceDigests,
symbols,
flags: "0x10a23001",
mips: {
elfClass: 32,
byteOrder: "little",
type: 2,
flags: 0x10a23001,
abiFlags,
},
mips,
abi_record_hex_dump: abiDump.stdout,
global_value: globalValue,
versions,
@@ -115,8 +106,8 @@ export async function buildPspFixture(directory, globalValue = 7) {
};
}
/** Independent GNU fields for the pinned PSPSDK fixture, not REA's parser. */
export function parsePspReadelfAbi(text) {
/** Complete independent fields for the pinned PSPSDK fixture, not REA's parser. */
export function parsePspReadelfMips(text) {
const field = (expression, label) => {
const matches = [...text.matchAll(expression)];
assert.equal(
@@ -126,6 +117,13 @@ export function parsePspReadelfAbi(text) {
);
return matches[0][1].trim();
};
assert.equal(field(/^\s*Class:\s*(.+)$/gmu, "class"), "ELF32");
assert.equal(
field(/^\s*Data:.*?,\s*(little|big) endian\s*$/gmu, "byte order"),
"little",
);
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
assert.match(field(/^\s*Flags:\s*(.+)$/gmu, "flags"), /^0x10a23001\b/u);
const width = (name) => {
const bits = Number(
field(new RegExp(`^${name} size:\\s*(\\d+)\\s*$`, "gmu"), name),
@@ -178,7 +176,49 @@ export function parsePspReadelfAbi(text) {
},
"PSPSDK ABI declaration differs from independently inspected build intent",
);
return abi;
return {
elfClass: 32,
byteOrder: "little",
type: 2,
flags: 0x10a23001,
abiFlags: abi,
gnuAttributes: parsePspGnuAttributes(text),
};
}
// The maintained PSP fixture is single-float. Retain that independently decoded
// GNU declaration when present, and refuse other tags/vendors/scopes rather
// than confusing an uninspected or unsupported declaration with absence.
function parsePspGnuAttributes(text) {
const count = parseMipsReadelfSectionTypes(text).filter(
(type) => type === "GNU_ATTRIBUTES",
).length;
const headers = [...text.matchAll(/^Attribute Section: (.+)$/gmu)];
if (count === 0) {
assert.equal(
headers.length,
0,
"GNU attributes without a declared section",
);
return null;
}
assert.equal(count, 1, "Ambiguous GNU attributes sections");
assert.equal(headers.length, 1, "Missing or ambiguous GNU attributes output");
const header = headers[0];
assert.equal(header[1].trim(), "gnu", "Unsupported GNU attributes vendor");
const tail = text.slice(header.index + header[0].length);
// GNU attributes may precede or follow the architecture-specific ABI dump.
const end = tail.search(/^MIPS ABI Flags Version:/mu);
const lines = (end < 0 ? tail : tail.slice(0, end))
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
assert.deepEqual(
lines,
["File Attributes", "Tag_GNU_MIPS_ABI_FP: Hard float (single precision)"],
"Unsupported or incomplete PSP GNU attributes declaration",
);
return { fpAbi: 2, msa: null };
}
/** Observations from Ghidra, not an echo of the requested profile. */