fix(pe): retain resource directory branch identities

This commit is contained in:
morluto
2026-10-24 02:29:37 +08:00
parent c46064c3f6
commit 66671e8748
4 changed files with 41 additions and 0 deletions
+4
View File
@@ -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
+2
View File
@@ -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,
+33
View File
@@ -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",
+2
View File
@@ -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),