fix(beam): distinguish empty optional chunk payloads

This commit is contained in:
morluto
2026-10-24 02:29:36 +08:00
parent 06032237c6
commit c46064c3f6
3 changed files with 68 additions and 9 deletions
+7 -1
View File
@@ -37,9 +37,15 @@ When `AtU8` or `Atom` is present, `atoms` is the 1-based name table and
those names when the table is present; unresolved indices keep `function` or
`module` null.
A zero-byte `AtU8` chunk does not override a nonempty historical `Atom` table.
The chunk inventory retains both occurrences. A nonempty malformed `AtU8`
still fails instead of falling back to an older table.
`code` reports the Code chunk instruction-set, opcode maximum, label count,
function count, and the instruction stream's file range. Missing optional
tables are null, not empty-everywhere.
tables are null, not empty-everywhere. A zero-byte optional `FunT` has no decoded
lambda table (`lambdas: null`); a four-byte zero count is an observed empty table
(`lambdas: []`). Both chunks remain in the inventory.
## Limits and failures
+46
View File
@@ -243,6 +243,52 @@ it("keeps a latin1 atom byte unchanged", () => {
expect(report.module).toBe("ÿ");
});
it("uses a nonempty legacy atom table when the selected AtU8 chunk is empty", () => {
const legacy = singleChunkBeam("Atom", Buffer.from([0, 0, 0, 1, 1, 0xff]));
const empty = singleChunkBeam("AtU8", Buffer.alloc(0));
for (const chunks of [
[legacy, empty],
[empty, legacy],
]) {
const bytes = Buffer.concat([chunks[0]!, chunks[1]!.subarray(12)]);
bytes.writeUInt32BE(bytes.length - 8, 4);
expect(parse(bytes)).toMatchObject({
atom_encoding: "latin1",
module: "ÿ",
chunks: expect.arrayContaining([
expect.objectContaining({
name: "AtU8",
payload: expect.objectContaining({ bytes: 0 }),
}),
]),
});
}
});
it("retains empty optional FunT chunks without discarding compiler module facts", () => {
const original = erlangBeamOtp29Fixture();
const control = parse(original);
for (const payload of [Buffer.alloc(0), Buffer.alloc(4), Buffer.alloc(1)]) {
const bytes = Buffer.concat([
original,
singleChunkBeam("FunT", payload).subarray(12),
]);
bytes.writeUInt32BE(bytes.length - 8, 4);
if (payload.length === 1) {
expect(() => parse(bytes)).toThrow(/lambda table is truncated/);
continue;
}
const report = parse(bytes);
expect(report.module).toBe(control.module);
expect(report.exports).toEqual(control.exports);
expect(report.lambdas).toEqual(payload.length === 0 ? null : []);
expect(report.chunks.at(-1)).toMatchObject({
name: "FunT",
payload: { bytes: payload.length },
});
}
});
it("rejects combined table rows before allocating them", () => {
const below = Math.floor(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS / 2);
const overflow = MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS - below * 2 + 1;
+15 -8
View File
@@ -256,12 +256,18 @@ export const parseErlangBeam = (
payloads.set(name, { payload, offset: at });
at = padded;
}
const atomChunk = payloads.get("AtU8") ?? payloads.get("Atom");
const atomEncoding = payloads.has("AtU8")
? "utf8"
: payloads.has("Atom")
? "latin1"
: null;
const utf8Atoms = payloads.get("AtU8");
const legacyAtoms = payloads.get("Atom");
const useLegacyAtoms =
(utf8Atoms?.payload.length ?? 0) === 0 &&
(legacyAtoms?.payload.length ?? 0) > 0;
const atomChunk = useLegacyAtoms ? legacyAtoms : (utf8Atoms ?? legacyAtoms);
const atomEncoding =
atomChunk === undefined
? null
: useLegacyAtoms || utf8Atoms === undefined
? "latin1"
: "utf8";
const atoms =
atomChunk === undefined || atomEncoding === null
? null
@@ -300,10 +306,11 @@ export const parseErlangBeam = (
function: atomAt(atoms, row[1]!),
arity: row[2]!,
}));
const lambdaPayload = payloads.get("FunT")?.payload;
const lambdas =
payloads.get("FunT") === undefined
lambdaPayload === undefined || lambdaPayload.length === 0
? null
: parseRows(payloads.get("FunT")!.payload, 6, "lambda").map((row) => ({
: parseRows(lambdaPayload, 6, "lambda").map((row) => ({
function_index: row[0]!,
function: atomAt(atoms, row[0]!),
arity: row[1]!,