diff --git a/docs/erlang-beam.md b/docs/erlang-beam.md index 6317dd2f92..f2528c2652 100644 --- a/docs/erlang-beam.md +++ b/docs/erlang-beam.md @@ -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 diff --git a/src/erlang/BeamParser.test.ts b/src/erlang/BeamParser.test.ts index 5051e52590..bd1f3fe4cd 100644 --- a/src/erlang/BeamParser.test.ts +++ b/src/erlang/BeamParser.test.ts @@ -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; diff --git a/src/erlang/BeamParser.ts b/src/erlang/BeamParser.ts index 47c52d8995..3a60727547 100644 --- a/src/erlang/BeamParser.ts +++ b/src/erlang/BeamParser.ts @@ -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]!,