fix(erlang): select the last repeated BEAM chunk

Fixes #6518
This commit is contained in:
morluto committed 2026-10-23 17:14:38 +08:00
1 parent 0c99194b8c
commit f28c0fdfb0
4 files changed
+94 -2

No files matched your search

+6 -1
View File
@@ -24,6 +24,9 @@ requires an absolute filesystem path and preserves that exact selection:
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`chunks` lists every IFF chunk name, file range, and payload SHA-256.
Repeated names retain separate chunk occurrences. Decoded tables and the Code
header use the last occurrence of each name, matching OTP's loader; the chunk
inventory still exposes earlier payloads and their source ranges.
When `AtU8` or `Atom` is present, `atoms` is the 1-based name table and
`module` is atom 1. Both the pre-OTP 28 table and the OTP 28+ compact table
@@ -43,4 +46,6 @@ import, and lambda rows together. Malformed `FOR1`/`BEAM` headers, truncated
chunks, and `AtU8` names that are not valid UTF-8 fail without partial success.
This workflow does not execute bytecode, inflate `LitT` literals, or decode
`Attr`/`CInf` external-term payloads.
`Attr`/`CInf` external-term payloads. Complete coverage describes this static
chunk and table report; it does not establish that an Erlang runtime can load
or execute the module.
+46
View File
@@ -79,6 +79,52 @@ it("reads atoms, exports, imports, lambdas and the Code header", () => {
]);
});
it("selects the last table and Code payload for every repeated chunk name", () => {
const original = erlangBeamFixture("selected");
const decoy = erlangBeamFixture("decoy");
const changedFields = new Map([
["Code", 16],
["ExpT", 8],
["ImpT", 12],
["FunT", 8],
]);
for (let at = 12; at < decoy.length;) {
const name = decoy.toString("latin1", at, at + 4);
const size = decoy.readUInt32BE(at + 4);
const field = changedFields.get(name);
if (field !== undefined) decoy.writeUInt32BE(9, at + 8 + field);
at += 8 + size + ((4 - (size % 4)) % 4);
}
const repeated = Buffer.concat([
original.subarray(0, 12),
decoy.subarray(12),
original.subarray(12),
]);
repeated.writeUInt32BE(repeated.length - 8, 4);
const report = parse(repeated);
const control = parse(original);
expect(report.atoms).toEqual(control.atoms);
expect(report.exports).toEqual(control.exports);
expect(report.imports).toEqual(control.imports);
expect(report.lambdas).toEqual(control.lambdas);
expect(report.code).toEqual({
...control.code,
instructions: {
...control.code!.instructions,
offset: control.code!.instructions.offset + decoy.length - 12,
},
});
expect(report.chunks).toHaveLength(control.chunks.length * 2);
});
it("rejects a malformed selected atom table instead of using an earlier table", () => {
const original = erlangBeamFixture();
const invalid = singleChunkBeam("AtU8", Buffer.alloc(0));
const repeated = Buffer.concat([original, invalid.subarray(12)]);
repeated.writeUInt32BE(repeated.length - 8, 4);
expect(() => parse(repeated)).toThrow(/truncated/);
});
// Expected values come from beam_lib:chunks/2 on the same file under OTP 29.
it("reads a compiler-produced OTP 29 BEAM with the compact atom table", () => {
const report = parse(erlangBeamOtp29Fixture());
+3 -1
View File
@@ -256,7 +256,9 @@ export const parseErlangBeam = (
sha256: sha256(payload),
},
});
if (!payloads.has(name)) payloads.set(name, { payload, offset: at });
// OTP's loader replaces the selected payload on each matching chunk.
// Keep every occurrence above; decode the last occurrence of each name.
payloads.set(name, { payload, offset: at });
at = padded;
}
const atomChunk = payloads.get("AtU8") ?? payloads.get("Atom");
+39
View File
@@ -72,6 +72,45 @@ cliTest("reports BEAM exports through the public CLI", async ({ cli }) => {
).toBe(true);
});
cliTest(
"decodes the last atom table while retaining every chunk occurrence",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-beam-repeated-atoms-");
const original = erlangBeamOtp29Fixture();
const decoy = oneAtomBeam(0).subarray(12);
const repeated = Buffer.concat([
original.subarray(0, 12),
decoy,
original.subarray(12),
]);
repeated.writeUInt32BE(repeated.length - 8, 4);
const path = join(root, "repeated.beam");
await writeFile(path, repeated);
const result = await cli.run({
arguments: ["inspect-erlang-beam", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const report = toolContract("inspect_erlang_beam").outputSchema.parse(
result.json,
).normalized_result;
expect(report.module).toBe("rea_beam_fixture");
expect(report.exports).toContainEqual({
function_index: 2,
function: "hello",
arity: 1,
label: 2,
});
const atomChunks = report.chunks.filter(({ name }) => name === "AtU8");
expect(atomChunks).toHaveLength(2);
expect(atomChunks[0]?.payload.sha256).not.toBe(
atomChunks[1]?.payload.sha256,
);
expect(atomChunks[0]?.payload.location.offset).toBe(20);
expect(atomChunks[1]?.payload.location.offset).toBe(20 + decoy.length);
expect(report.coverage.status).toBe("complete");
},
);
cliTest(
"inspects an OTP 29 BEAM with an extended compact atom length and preserves source identity",
async ({ cli }) => {