diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index b974ca32c9..24c63499b2 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -203,6 +203,10 @@ reported. Directory headers, entries, data entries and payloads retain original file offsets and lengths. Each opaque payload has its own SHA-256; code page and reserved data-entry fields are preserved. +Each `directories[]` row retains its incoming identity `path` and corresponding +`entry_locations`, including branches with no payload leaves. The root has empty +arrays; a type branch has one identity and a name branch has two. + RT_GROUP_ICON records expose original image descriptors and RT_ICON candidates. A reference resolves only when an icon with the same numeric ID and language exists. Missing references, other-language candidates and declared-size mismatch diff --git a/src/domain/native/peResources.ts b/src/domain/native/peResources.ts index 91b669f18a..ed5f59bc9d 100644 --- a/src/domain/native/peResources.ts +++ b/src/domain/native/peResources.ts @@ -65,6 +65,8 @@ export const peResourcesSchema = z.strictObject({ .nullable(), directories: z.array( z.strictObject({ + path: z.array(identity).max(2), + entry_locations: z.array(range).max(2), location: range, characteristics: uint, timestamp: uint, diff --git a/src/native/pe/PeResourceParser.test.ts b/src/native/pe/PeResourceParser.test.ts index ecf3ec4195..c1d1fafe11 100644 --- a/src/native/pe/PeResourceParser.test.ts +++ b/src/native/pe/PeResourceParser.test.ts @@ -143,6 +143,39 @@ describe("portable PE resource parsing", () => { }); }); + it.each([10, 11, "named type"])( + "retains identity and incoming entry ranges for empty branch %s", + async (type) => { + const fixture = peResourceFixture(false, [ + { type, name: 7, language: 1033, payload: Buffer.from("control") }, + ]); + const populated = peResourcesSchema.parse(await parse(fixture.bytes)); + expect(populated.resources[0]?.type).toMatchObject( + typeof type === "number" + ? { kind: "id", id: type } + : { kind: "name", name: type }, + ); + // Empty the type directory while retaining its incoming root entry. + const typeDirectory = fixture.directoryOffsets[1]!; + fixture.bytes.writeUInt16LE(0, typeDirectory + 12); + fixture.bytes.writeUInt16LE(0, typeDirectory + 14); + const report = peResourcesSchema.parse(await parse(fixture.bytes)); + expect(report.resources).toEqual([]); + expect(report.directories.map(({ path }) => path)).toEqual([ + [], + [populated.resources[0]!.type], + ]); + expect(report.directories[1]?.entry_locations).toEqual([ + populated.resources[0]!.entry_locations[0], + ]); + expect(report.coverage).toEqual({ + status: "complete", + examined_entries: 1, + resources: 0, + }); + }, + ); + it.each([ [ "truncated directory", diff --git a/src/native/pe/PeResourceParser.ts b/src/native/pe/PeResourceParser.ts index 6a56ac15c2..271b67d68c 100644 --- a/src/native/pe/PeResourceParser.ts +++ b/src/native/pe/PeResourceParser.ts @@ -132,6 +132,8 @@ export const parsePeResources = async ( examined += count; relative(at, 16 + 8 * count); const directory = { + path, + entry_locations: entryLocations, location: recordRange(header, 16 + 8 * count, "directory"), characteristics: bytes.readUInt32LE(header), timestamp: bytes.readUInt32LE(header + 4),