refactor(managed): share decoded type definition facts
This commit is contained in:
1 parent
f276bcbf6c
commit
d0432d63c9
4 files changed
+145
-124
No files matched your search
@@ -2245,13 +2245,15 @@ it.each([
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ facet: "fields", nameOffset: 2 },
|
||||
{ facet: "methods", nameOffset: 8 },
|
||||
{ facet: "member_refs", nameOffset: 2 },
|
||||
{ facet: "types", nameOffset: 4 },
|
||||
{ facet: "fields", nameOffsets: [2] },
|
||||
{ facet: "methods", nameOffsets: [8] },
|
||||
{ facet: "member_refs", nameOffsets: [2] },
|
||||
{ facet: "types", nameOffsets: [4] },
|
||||
{ facet: "types", nameOffsets: [6] },
|
||||
{ facet: "types", nameOffsets: [4, 6] },
|
||||
] as const)(
|
||||
"preserves independent metadata when a $facet name is unreadable",
|
||||
({ facet, nameOffset }) => {
|
||||
"preserves independent metadata when $facet name columns $nameOffsets are unreadable",
|
||||
({ facet, nameOffsets }) => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const original = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
@@ -2259,7 +2261,8 @@ it.each([
|
||||
);
|
||||
const row = original[facet][0]?.row_offset;
|
||||
if (row === undefined) throw new Error(`Fixture must contain ${facet}`);
|
||||
bytes.writeUInt16LE(0xffff, row + nameOffset);
|
||||
for (const [index, nameOffset] of nameOffsets.entries())
|
||||
bytes.writeUInt16LE(0xffff - index, row + nameOffset);
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
@@ -2278,13 +2281,17 @@ it.each([
|
||||
expect(result[remaining]).toHaveLength(original[remaining].length);
|
||||
expect(result.coverage).toMatchObject({
|
||||
state: "partial",
|
||||
issues: [
|
||||
issues: nameOffsets.map(() =>
|
||||
expect.objectContaining({
|
||||
code: "invalid-heap-index",
|
||||
scope: "metadata.#Strings",
|
||||
offset: expect.any(Number),
|
||||
}),
|
||||
],
|
||||
),
|
||||
});
|
||||
expect(
|
||||
new Set(result.coverage.issues.map(({ offset }) => offset)).size,
|
||||
).toBe(nameOffsets.length);
|
||||
if (facet === "types") {
|
||||
expect(result.fields[0]).toMatchObject({
|
||||
declaring_type_token: "0x02000001",
|
||||
@@ -2327,25 +2334,32 @@ describe("managed member type selection", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat a name as absent when a TypeDef name is unreadable", () => {
|
||||
const corrupt = Buffer.from(bytes);
|
||||
const row = everything.types[0]?.row_offset ?? 0;
|
||||
// Point the TypeDef Name past the #Strings heap.
|
||||
corrupt.writeUInt16LE(0xffff, row + 4);
|
||||
const result = inspectManagedMembersBytes(
|
||||
corrupt,
|
||||
managedPeFixtureTarget(corrupt),
|
||||
{ typeNames: ["Fixture.Program"] },
|
||||
);
|
||||
it.each([
|
||||
{ column: "name", offset: 4 },
|
||||
{ column: "namespace", offset: 6 },
|
||||
])(
|
||||
"does not treat a name as absent when a TypeDef $column is unreadable",
|
||||
({ offset }) => {
|
||||
const corrupt = Buffer.from(bytes);
|
||||
const row = everything.types[0]?.row_offset ?? 0;
|
||||
corrupt.writeUInt16LE(0xffff, row + offset);
|
||||
const result = inspectManagedMembersBytes(
|
||||
corrupt,
|
||||
managedPeFixtureTarget(corrupt),
|
||||
{ typeNames: ["Fixture.Program"] },
|
||||
);
|
||||
|
||||
expect(result.selection?.unmatched_type_names).toEqual(["Fixture.Program"]);
|
||||
expect(result.coverage.state).toBe("partial");
|
||||
// Its methods were never inspected, so neither were their literals.
|
||||
expect(result.user_strings?.state).toBe("partial");
|
||||
expect(result.limitations).toContain(
|
||||
"1 TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.",
|
||||
);
|
||||
});
|
||||
expect(result.selection?.unmatched_type_names).toEqual([
|
||||
"Fixture.Program",
|
||||
]);
|
||||
expect(result.coverage.state).toBe("partial");
|
||||
// Its methods were never inspected, so neither were their literals.
|
||||
expect(result.user_strings?.state).toBe("partial");
|
||||
expect(result.limitations).toContain(
|
||||
"1 TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("omits members of unselected types without dropping member references", () => {
|
||||
const result = inspectManagedMembersBytes(bytes, target, {
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "./ManagedReaderFailure.js";
|
||||
import {
|
||||
parseTypes,
|
||||
typeRanges,
|
||||
readTypeDefinitions,
|
||||
type TypeRange,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import { edgeTargets } from "./ManagedMethodSpecs.js";
|
||||
@@ -190,7 +190,7 @@ export const inspectManagedMembersBytes = (
|
||||
const { layout, memberLists } = readMemberLayout(bytes, pe);
|
||||
const identity = readManagedModuleIdentity(bytes, layout);
|
||||
appendManagedParseIssues(issues, identity.issues);
|
||||
const ranges = typeRanges(bytes, layout, issues, memberLists);
|
||||
const ranges = readTypeDefinitions(bytes, layout, issues, memberLists);
|
||||
const {
|
||||
tokens: selected,
|
||||
selection,
|
||||
|
||||
@@ -60,6 +60,13 @@ export interface TypeRange {
|
||||
readonly methodEnd: number | null;
|
||||
}
|
||||
|
||||
/** One TypeDef row's names and coordinates, including unreadable declarations. */
|
||||
interface TypeDefinition extends TypeRange {
|
||||
readonly row: number;
|
||||
readonly name: string | undefined;
|
||||
readonly namespace: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name and declaring type of a field, method, member reference, or method
|
||||
* specification, keyed by its token so edges resolve any target uniformly.
|
||||
@@ -109,36 +116,38 @@ const rowRange = (
|
||||
export const fullName = (namespace: string, name: string): string =>
|
||||
namespace.length === 0 ? name : `${namespace}.${name}`;
|
||||
|
||||
const readTypeRange = (
|
||||
const readTypeDefinition = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
issues: ManagedParseIssue[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
): TypeRange => {
|
||||
): TypeDefinition => {
|
||||
const cursor = metadataRowCursor(bytes, layout, 2, row);
|
||||
cursor.readUInt32();
|
||||
const nameIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
const declaredName =
|
||||
readManagedValue(
|
||||
() =>
|
||||
fullName(
|
||||
readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
namespaceIndex,
|
||||
layout.strings.size,
|
||||
),
|
||||
readMetadataString(bytes, layout, nameIndex, layout.strings.size),
|
||||
),
|
||||
issues,
|
||||
) ?? null;
|
||||
// Keep both independent name failures, but decode each heap coordinate once.
|
||||
const namespace = readManagedValue(
|
||||
() =>
|
||||
readMetadataString(bytes, layout, namespaceIndex, layout.strings.size),
|
||||
issues,
|
||||
);
|
||||
const name = readManagedValue(
|
||||
() => readMetadataString(bytes, layout, nameIndex, layout.strings.size),
|
||||
issues,
|
||||
);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
const current = memberLists.entries[row - 1];
|
||||
return {
|
||||
row,
|
||||
name,
|
||||
namespace,
|
||||
token: metadataToken(2, row),
|
||||
fullName: declaredName,
|
||||
fullName:
|
||||
namespace === undefined || name === undefined
|
||||
? null
|
||||
: fullName(namespace, name),
|
||||
fieldStart: current?.field.start.value ?? null,
|
||||
fieldEnd: current?.field.end ?? null,
|
||||
methodStart: current?.method.start.value ?? null,
|
||||
@@ -146,18 +155,20 @@ const readTypeRange = (
|
||||
};
|
||||
};
|
||||
|
||||
export const typeRanges = (
|
||||
export const readTypeDefinitions = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
issues: ManagedParseIssue[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
): readonly TypeRange[] => {
|
||||
): readonly TypeDefinition[] => {
|
||||
issues.push(...memberLists.issues);
|
||||
const typeTable = layout.table(2);
|
||||
const ranges: TypeRange[] = [];
|
||||
const definitions: TypeDefinition[] = [];
|
||||
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1)
|
||||
ranges.push(readTypeRange(bytes, layout, row, issues, memberLists));
|
||||
return ranges;
|
||||
definitions.push(
|
||||
readTypeDefinition(bytes, layout, row, issues, memberLists),
|
||||
);
|
||||
return definitions;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -167,91 +178,82 @@ export const typeRanges = (
|
||||
export const parseTypes = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
ranges: readonly TypeRange[],
|
||||
definitions: readonly TypeDefinition[],
|
||||
): {
|
||||
readonly types: readonly ManagedType[];
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
readonly nesting: ManagedTypeNesting;
|
||||
} => {
|
||||
const typeTable = layout.table(2);
|
||||
const fields = memberListTable(layout, 4);
|
||||
const methods = memberListTable(layout, 6);
|
||||
const items: ManagedType[] = [];
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const nesting = readManagedTypeNesting(bytes, layout, issues);
|
||||
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) {
|
||||
readManagedValue(() => {
|
||||
const cursor = metadataRowCursor(bytes, layout, 2, row);
|
||||
const flags = cursor.readUInt32();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
layout.strings.size,
|
||||
);
|
||||
const namespace = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
layout.strings.size,
|
||||
);
|
||||
const extendsOffset = cursor.offset;
|
||||
const extendsRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("TypeDefOrRef"),
|
||||
);
|
||||
const currentRange = ranges[row - 1] ?? null;
|
||||
const fieldRange = rowRange(
|
||||
fields,
|
||||
currentRange?.fieldStart ?? null,
|
||||
currentRange?.fieldEnd ?? null,
|
||||
);
|
||||
const methodRange = rowRange(
|
||||
methods,
|
||||
currentRange?.methodStart ?? null,
|
||||
currentRange?.methodEnd ?? null,
|
||||
);
|
||||
const extendsReason = metadataCodedTokenInvalidReason(
|
||||
for (const definition of definitions) {
|
||||
const { row, name, namespace, token } = definition;
|
||||
if (
|
||||
name === undefined ||
|
||||
namespace === undefined ||
|
||||
definition.fullName === null
|
||||
)
|
||||
continue;
|
||||
const cursor = metadataRowCursor(bytes, layout, 2, row);
|
||||
const flags = cursor.readUInt32();
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
const extendsOffset = cursor.offset;
|
||||
const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
const fieldRange = rowRange(
|
||||
fields,
|
||||
definition.fieldStart,
|
||||
definition.fieldEnd,
|
||||
);
|
||||
const methodRange = rowRange(
|
||||
methods,
|
||||
definition.methodStart,
|
||||
definition.methodEnd,
|
||||
);
|
||||
const extendsReason = metadataCodedTokenInvalidReason(
|
||||
extendsRaw,
|
||||
2,
|
||||
[2, 1, 27],
|
||||
layout.rowCounts,
|
||||
);
|
||||
if (extendsReason !== null)
|
||||
issues.push({
|
||||
code: "invalid-row",
|
||||
scope: `metadata.TypeDef:${token}`,
|
||||
offset: extendsOffset,
|
||||
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`,
|
||||
});
|
||||
const enclosingRow = nesting.enclosing.get(row);
|
||||
items.push({
|
||||
token,
|
||||
row_offset: cursor.start,
|
||||
namespace,
|
||||
name,
|
||||
full_name: definition.fullName,
|
||||
flags,
|
||||
extends_token: metadataCodedToken(
|
||||
extendsRaw,
|
||||
2,
|
||||
[2, 1, 27],
|
||||
layout.rowCounts,
|
||||
);
|
||||
if (extendsReason !== null)
|
||||
issues.push({
|
||||
code: "invalid-row",
|
||||
scope: `metadata.TypeDef:${metadataToken(2, row)}`,
|
||||
offset: extendsOffset,
|
||||
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`,
|
||||
});
|
||||
const enclosingRow = nesting.enclosing.get(row);
|
||||
items.push({
|
||||
token: metadataToken(2, row),
|
||||
row_offset: cursor.start,
|
||||
namespace,
|
||||
name,
|
||||
full_name: fullName(namespace, name),
|
||||
flags,
|
||||
extends_token: metadataCodedToken(
|
||||
extendsRaw,
|
||||
2,
|
||||
[2, 1, 27],
|
||||
layout.rowCounts,
|
||||
),
|
||||
enclosing_type_token:
|
||||
enclosingRow === undefined ? null : metadataToken(2, enclosingRow),
|
||||
name_sha256: null,
|
||||
field_list: {
|
||||
first_row: fieldRange.first,
|
||||
last_row: fieldRange.last,
|
||||
count: fieldRange.count,
|
||||
},
|
||||
method_list: {
|
||||
first_row: methodRange.first,
|
||||
last_row: methodRange.last,
|
||||
count: methodRange.count,
|
||||
},
|
||||
});
|
||||
}, issues);
|
||||
),
|
||||
enclosing_type_token:
|
||||
enclosingRow === undefined ? null : metadataToken(2, enclosingRow),
|
||||
name_sha256: null,
|
||||
field_list: {
|
||||
first_row: fieldRange.first,
|
||||
last_row: fieldRange.last,
|
||||
count: fieldRange.count,
|
||||
},
|
||||
method_list: {
|
||||
first_row: methodRange.first,
|
||||
last_row: methodRange.last,
|
||||
count: methodRange.count,
|
||||
},
|
||||
});
|
||||
}
|
||||
return { types: items, issues, nesting };
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import {
|
||||
parseTypes,
|
||||
signature,
|
||||
typeRanges,
|
||||
readTypeDefinitions,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import {
|
||||
metadataRowOffset,
|
||||
@@ -169,7 +169,12 @@ describe("resolved member signature digests", () => {
|
||||
const parsedTypes = parseTypes(
|
||||
bytes,
|
||||
layout,
|
||||
typeRanges(bytes, layout, issues, readTypeMemberListFacts(bytes, layout)),
|
||||
readTypeDefinitions(
|
||||
bytes,
|
||||
layout,
|
||||
issues,
|
||||
readTypeMemberListFacts(bytes, layout),
|
||||
),
|
||||
);
|
||||
const resolve = managedTypeNameDigests(
|
||||
layout,
|
||||
|
||||
Reference in new issue
Block a user