mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(managed)!: share member list facts and retain verification uncertainty
Parse TypeDef member lists once at their owning boundary, retain malformed ranges as unknown, and admit representation storage before allocation. Reuse parsed facts across member ownership, inventory, attributes, and native boundaries. Preserve constant operands in structural matching. Refs: #3202, #3201, #3199 BREAKING CHANGE: unresolved managed/native verification results may retain a non-null matched_native and candidate facts when module identity is unknown. Consumers must use status and reason rather than assume unresolved observations are empty.
This commit is contained in:
@@ -323,6 +323,9 @@ function implements a specific managed method.
|
||||
`verify_managed_native_boundaries` compares declared P/Invoke module and entry
|
||||
point identities with supplied native export or function evidence. Name/module
|
||||
matches establish declaration links; they do not establish that a call executed.
|
||||
When either side lacks a usable module identity, a matching name remains
|
||||
unresolved and retains the candidate evidence; REA reports a module mismatch
|
||||
only when both identities are available and differ.
|
||||
|
||||
Metadata tokens are not native addresses. Native body mapping for C++/CLI,
|
||||
ReadyToRun, NativeAOT, and IL2CPP requires separately verified format and provider
|
||||
@@ -360,8 +363,9 @@ these indirections for member ownership and local attribute-constructor types;
|
||||
field and method tokens still identify the original definition rows.
|
||||
`field_list` and `method_list` retain the metadata's list coordinates, which
|
||||
refer to the pointer table when present rather than necessarily contiguous
|
||||
definition rows. Malformed or ambiguous pointers leave the owner unknown with
|
||||
partial coverage while preserving readable member and attribute facts.
|
||||
definition rows. Invalid or nonmonotonic list coordinates and malformed or
|
||||
ambiguous pointers leave affected owners unknown with partial coverage while
|
||||
preserving readable member and attribute facts.
|
||||
The retained inverse indexes are admitted against the existing representation
|
||||
budget before allocation.
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
|
||||
name: "verify_managed_native_boundaries",
|
||||
...toolContractMetadata("verify_managed_native_boundaries"),
|
||||
description:
|
||||
"Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states.",
|
||||
"Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states; a name candidate with missing managed or native module identity remains unresolved with its candidate evidence retained.",
|
||||
kind: "application",
|
||||
inputSchema: managedNativeVerificationInputSchema,
|
||||
outputSchema: nativeVerificationOutputSchema,
|
||||
|
||||
@@ -87,9 +87,10 @@ const keyMethod = (item: Method): Keyed<Method> => ({
|
||||
item.body.max_stack,
|
||||
item.body.init_locals,
|
||||
item.body.opcode_counts,
|
||||
item.body.anchors.map(({ opcode, operand_kind }) => [
|
||||
item.body.anchors.map(({ opcode, operand_kind, operand }) => [
|
||||
opcode,
|
||||
operand_kind,
|
||||
operand_kind === "constant" ? operand : null,
|
||||
]),
|
||||
item.body.exception_regions.map((region) => [
|
||||
region.flags,
|
||||
|
||||
@@ -124,13 +124,21 @@ const moduleCompatible = (
|
||||
scope: string | null,
|
||||
symbol: NativeSymbol,
|
||||
): boolean => {
|
||||
if (scope === null) return true;
|
||||
if (scope === null || scope.trim().length === 0) return false;
|
||||
const expected = normalizeModule(scope);
|
||||
return [symbol.module_name, symbol.module_path]
|
||||
.filter((value): value is string => value !== null)
|
||||
.filter(
|
||||
(value): value is string =>
|
||||
value !== null && normalizeModule(value).length > 0,
|
||||
)
|
||||
.some((value) => nativeModuleIdentities(value).has(expected));
|
||||
};
|
||||
|
||||
const hasModuleIdentity = (symbol: NativeSymbol): boolean =>
|
||||
[symbol.module_name, symbol.module_path].some(
|
||||
(value) => value !== null && normalizeModule(value).length > 0,
|
||||
);
|
||||
|
||||
const candidateNames = (
|
||||
managed: Pick<
|
||||
PinvokeImport,
|
||||
@@ -190,6 +198,9 @@ const matchPinvoke = (
|
||||
};
|
||||
|
||||
const candidates: [NativeSymbol, ...NativeSymbol[]] = [first, ...remaining];
|
||||
const scopeKnown =
|
||||
managed.import_scope_name !== null &&
|
||||
managed.import_scope_name.trim().length > 0;
|
||||
const exact = candidates.find(
|
||||
(symbol) =>
|
||||
managed.import_name === symbol.name &&
|
||||
@@ -198,17 +209,13 @@ const matchPinvoke = (
|
||||
const decorated = candidates.find((symbol) =>
|
||||
moduleCompatible(managed.import_scope_name, symbol),
|
||||
);
|
||||
const selected = exact ?? decorated ?? first;
|
||||
if (!moduleCompatible(managed.import_scope_name, selected))
|
||||
return {
|
||||
match: {
|
||||
status: "contradicted",
|
||||
basis: "module-mismatch",
|
||||
confidence: "unknown",
|
||||
matched_native: selected,
|
||||
candidates,
|
||||
},
|
||||
};
|
||||
const selected =
|
||||
exact ??
|
||||
decorated ??
|
||||
(scopeKnown
|
||||
? candidates.find((symbol) => !hasModuleIdentity(symbol))
|
||||
: undefined) ??
|
||||
first;
|
||||
if (exact !== undefined)
|
||||
return {
|
||||
match: {
|
||||
@@ -222,11 +229,31 @@ const matchPinvoke = (
|
||||
candidates,
|
||||
},
|
||||
};
|
||||
if (decorated !== undefined)
|
||||
return {
|
||||
match: {
|
||||
basis: "decorated-name-candidate",
|
||||
status: "inferred",
|
||||
confidence: "inferred",
|
||||
matched_native: selected,
|
||||
candidates,
|
||||
},
|
||||
};
|
||||
if (!scopeKnown || !hasModuleIdentity(selected))
|
||||
return {
|
||||
match: {
|
||||
status: "unresolved",
|
||||
basis: "module-identity-unknown",
|
||||
confidence: "unknown",
|
||||
matched_native: selected,
|
||||
candidates,
|
||||
},
|
||||
};
|
||||
return {
|
||||
match: {
|
||||
basis: "decorated-name-candidate",
|
||||
status: "inferred",
|
||||
confidence: "inferred",
|
||||
status: "contradicted",
|
||||
basis: "module-mismatch",
|
||||
confidence: "unknown",
|
||||
matched_native: selected,
|
||||
candidates,
|
||||
},
|
||||
@@ -234,26 +261,31 @@ const matchPinvoke = (
|
||||
};
|
||||
|
||||
const pinvokeLimitations = (
|
||||
status: PinvokeVerification["status"],
|
||||
match: PinvokeMatch,
|
||||
managed: PinvokeImport,
|
||||
): readonly string[] => [
|
||||
...(managed.import_scope_name === null
|
||||
...(managed.import_scope_name === null ||
|
||||
managed.import_scope_name.trim().length === 0
|
||||
? ["Managed declaration does not name an import scope/module."]
|
||||
: []),
|
||||
...(status === "inferred"
|
||||
...(match.status === "inferred"
|
||||
? [
|
||||
"Native symbol matched a decorated candidate rather than the exact declared import name.",
|
||||
]
|
||||
: []),
|
||||
...(status === "contradicted"
|
||||
...(match.status === "contradicted"
|
||||
? [
|
||||
"A symbol name candidate exists, but the supplied native Evidence module identity does not match the managed import scope.",
|
||||
]
|
||||
: []),
|
||||
...(status === "unresolved"
|
||||
? [
|
||||
"No supplied native export/function Evidence matched this declared import.",
|
||||
]
|
||||
...(match.status === "unresolved"
|
||||
? match.basis === "module-identity-unknown"
|
||||
? [
|
||||
"A native name candidate exists, but a required module identity is unavailable; module correspondence is unknown.",
|
||||
]
|
||||
: [
|
||||
"No supplied native export/function Evidence matched this declared import.",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
@@ -283,7 +315,7 @@ export const verifyPinvoke = ({
|
||||
declaration_verification: item.verification,
|
||||
};
|
||||
const selection = matchPinvoke(item, native.symbols, native.accepted);
|
||||
const limitations = pinvokeLimitations(selection.match.status, item);
|
||||
const limitations = pinvokeLimitations(selection.match, item);
|
||||
return {
|
||||
verification: pinvokeVerificationSchema.parse({
|
||||
item_id: `mnv_pinvoke_${digest({
|
||||
|
||||
@@ -51,7 +51,7 @@ const observedCandidatesSchema = z
|
||||
.tuple([nativeSymbolSchema])
|
||||
.rest(nativeSymbolSchema);
|
||||
|
||||
export const pinvokeVerificationSchema = z.discriminatedUnion("status", [
|
||||
export const pinvokeVerificationSchema = z.union([
|
||||
z.strictObject({
|
||||
...pinvokeVerificationContextShape,
|
||||
status: z.literal("verified"),
|
||||
@@ -76,6 +76,14 @@ export const pinvokeVerificationSchema = z.discriminatedUnion("status", [
|
||||
matched_native: z.null(),
|
||||
candidates: emptyArraySchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...pinvokeVerificationContextShape,
|
||||
status: z.literal("unresolved"),
|
||||
basis: z.literal("module-identity-unknown"),
|
||||
confidence: z.literal("unknown"),
|
||||
matched_native: nativeSymbolSchema,
|
||||
candidates: observedCandidatesSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...pinvokeVerificationContextShape,
|
||||
status: z.literal("contradicted"),
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
readManagedMetadataLayout,
|
||||
type ManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
|
||||
const flagNames = (flags: number): string[] => {
|
||||
@@ -384,10 +385,12 @@ export const inspectManagedArtifactBytes = (
|
||||
resourceIssues,
|
||||
);
|
||||
try {
|
||||
const memberLists = readTypeMemberListFacts(bytes, metadata);
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
metadata,
|
||||
resourceDirectory,
|
||||
memberLists,
|
||||
);
|
||||
const issues = [...resourceIssues, ...inventory.issues];
|
||||
const limitations = [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type ManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import { readManagedPeLayout } from "./ManagedPeReader.js";
|
||||
import { memberListIndexSize } from "./ManagedMemberPointers.js";
|
||||
import { edges } from "./ManagedMemberRows.js";
|
||||
import { parseUserStrings } from "./ManagedUserStrings.js";
|
||||
import { signature, type ManagedMethod } from "./ManagedMemberInspectorCore.js";
|
||||
@@ -1224,7 +1225,173 @@ describe("managed derived caller names", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member reference bounds", () => {
|
||||
describe("managed TypeDef list ownership bounds", () => {
|
||||
it("retains rows but leaves ownership unknown for zero TypeDef list starts", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const pe = readManagedPeLayout(bytes);
|
||||
if (pe.cli === null) throw new Error("fixture has no CLI header");
|
||||
const { rva, size } = pe.cli.metadata;
|
||||
const layout = readManagedMetadataLayout(
|
||||
bytes,
|
||||
pe.rvaToOffset(rva, size, "cli.metadata"),
|
||||
size,
|
||||
);
|
||||
const listOffset =
|
||||
metadataRowOffset(layout, 2, 1) +
|
||||
4 +
|
||||
2 * layout.stringIndexSize +
|
||||
layout.codedIndexSize("TypeDefOrRef");
|
||||
const listIndexWidth = Math.max(
|
||||
layout.tableIndexSize(3),
|
||||
layout.tableIndexSize(4),
|
||||
layout.tableIndexSize(5),
|
||||
layout.tableIndexSize(6),
|
||||
);
|
||||
const writeIndex = (offset: number) =>
|
||||
listIndexWidth === 2
|
||||
? bytes.writeUInt16LE(0, offset)
|
||||
: bytes.writeUInt32LE(0, offset);
|
||||
writeIndex(listOffset);
|
||||
writeIndex(listOffset + listIndexWidth);
|
||||
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.types[0]).toMatchObject({
|
||||
field_list: { first_row: null, last_row: null, count: 0 },
|
||||
method_list: { first_row: null, last_row: null, count: 0 },
|
||||
});
|
||||
expect(result.fields[0]).toMatchObject({
|
||||
token: "0x04000001",
|
||||
name: "counter",
|
||||
declaring_type: null,
|
||||
});
|
||||
expect(result.methods[0]).toMatchObject({
|
||||
token: "0x06000001",
|
||||
name: "Main",
|
||||
declaring_type: null,
|
||||
});
|
||||
expect(result.coverage).toMatchObject({
|
||||
state: "partial",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
code: "invalid-row",
|
||||
detail: expect.stringContaining("TypeDef FieldList start 0"),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-row",
|
||||
detail: expect.stringContaining("TypeDef MethodList start 0"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not infer an earlier owner from an invalid next list endpoint", () => {
|
||||
const bytes = buildManagedPeFixture({ additionalEmptyTypes: 1 });
|
||||
const pe = readManagedPeLayout(bytes);
|
||||
if (pe.cli === null) throw new Error("fixture has no CLI header");
|
||||
const { rva, size } = pe.cli.metadata;
|
||||
const layout = readManagedMetadataLayout(
|
||||
bytes,
|
||||
pe.rvaToOffset(rva, size, "cli.metadata"),
|
||||
size,
|
||||
);
|
||||
const methodStartOffset =
|
||||
metadataRowOffset(layout, 2, 2) +
|
||||
4 +
|
||||
2 * layout.stringIndexSize +
|
||||
layout.codedIndexSize("TypeDefOrRef") +
|
||||
memberListIndexSize(layout, 4);
|
||||
if (memberListIndexSize(layout, 6) === 2)
|
||||
bytes.writeUInt16LE(0, methodStartOffset);
|
||||
else bytes.writeUInt32LE(0, methodStartOffset);
|
||||
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.methods[0]).toMatchObject({
|
||||
token: "0x06000001",
|
||||
declaring_type: null,
|
||||
});
|
||||
expect(result.types[0]?.method_list).toMatchObject({
|
||||
first_row: null,
|
||||
last_row: null,
|
||||
count: 0,
|
||||
});
|
||||
expect(result.coverage.state).toBe("partial");
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed TypeDef list ordering", () => {
|
||||
it("leaves adjacent ownership unknown when valid list starts decrease", () => {
|
||||
const bytes = buildManagedPeFixture({
|
||||
additionalEmptyTypes: 1,
|
||||
methodRowCount: 2,
|
||||
});
|
||||
const pe = readManagedPeLayout(bytes);
|
||||
if (pe.cli === null) throw new Error("fixture has no CLI header");
|
||||
const { rva, size } = pe.cli.metadata;
|
||||
const layout = readManagedMetadataLayout(
|
||||
bytes,
|
||||
pe.rvaToOffset(rva, size, "cli.metadata"),
|
||||
size,
|
||||
);
|
||||
const methodOffset =
|
||||
4 +
|
||||
2 * layout.stringIndexSize +
|
||||
layout.codedIndexSize("TypeDefOrRef") +
|
||||
memberListIndexSize(layout, 4);
|
||||
const writeMethodStart = (row: number, value: number): void => {
|
||||
const offset = metadataRowOffset(layout, 2, row) + methodOffset;
|
||||
if (memberListIndexSize(layout, 6) === 2)
|
||||
bytes.writeUInt16LE(value, offset);
|
||||
else bytes.writeUInt32LE(value, offset);
|
||||
};
|
||||
writeMethodStart(1, 2);
|
||||
writeMethodStart(2, 1);
|
||||
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
|
||||
expect(result.methods.map(({ declaring_type }) => declaring_type)).toEqual([
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
expect(result.coverage).toMatchObject({
|
||||
state: "partial",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
code: "invalid-row",
|
||||
detail: expect.stringContaining(
|
||||
"MethodList start 1 decreases from preceding start 2",
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts the one-based start for an empty ordinary member table", () => {
|
||||
const bytes = buildManagedPeFixture({ methodRowCount: 0 });
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
expect(result.types[0]?.method_list).toEqual({
|
||||
first_row: null,
|
||||
last_row: null,
|
||||
count: 0,
|
||||
});
|
||||
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed metadata reference bounds", () => {
|
||||
it("retains members and reports an out-of-range TypeDef coded index as partial", () => {
|
||||
const bytes = buildManagedPeFixture({ extendsRaw: 12 });
|
||||
const result = inspectManagedMembersBytes(
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { edgeTargets } from "./ManagedMethodSpecs.js";
|
||||
import { typeNameLookup } from "./ManagedTypeNames.js";
|
||||
import { parseUserStrings } from "./ManagedUserStrings.js";
|
||||
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
import {
|
||||
edges,
|
||||
parseExportedTypes,
|
||||
@@ -113,12 +114,14 @@ const readMemberInventory = (
|
||||
rootOffset,
|
||||
cli.metadata.size,
|
||||
);
|
||||
const memberLists = readTypeMemberListFacts(bytes, layout);
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
layout,
|
||||
readManagedResourceDirectory(pe, issues),
|
||||
memberLists,
|
||||
);
|
||||
return { layout, inventory };
|
||||
return { layout, inventory, memberLists };
|
||||
};
|
||||
|
||||
const selectTypes = (
|
||||
@@ -185,9 +188,13 @@ export const inspectManagedMembersBytes = (
|
||||
return unavailable(target, bytes, pe.cliIssue, typeNames);
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
try {
|
||||
const { layout, inventory } = readMemberInventory(bytes, pe, issues);
|
||||
const { layout, inventory, memberLists } = readMemberInventory(
|
||||
bytes,
|
||||
pe,
|
||||
issues,
|
||||
);
|
||||
appendManagedParseIssues(issues, inventory.issues);
|
||||
const ranges = typeRanges(bytes, layout, issues);
|
||||
const ranges = typeRanges(bytes, layout, issues, memberLists);
|
||||
const {
|
||||
tokens: selected,
|
||||
selection,
|
||||
@@ -197,7 +204,7 @@ export const inspectManagedMembersBytes = (
|
||||
appendManagedParseIssues(issues, parsedTypes.issues);
|
||||
const exportedTypes = parseExportedTypes(bytes, layout);
|
||||
appendManagedParseIssues(issues, exportedTypes.issues);
|
||||
const fields = parseFields(bytes, layout, ranges, selected);
|
||||
const fields = parseFields(bytes, layout, ranges, memberLists, selected);
|
||||
const memberRefs = parseMemberRefs(
|
||||
bytes,
|
||||
layout,
|
||||
@@ -209,6 +216,7 @@ export const inspectManagedMembersBytes = (
|
||||
layout,
|
||||
pe,
|
||||
ranges,
|
||||
memberLists,
|
||||
selected,
|
||||
methodOffset,
|
||||
methodLimit,
|
||||
|
||||
@@ -20,10 +20,8 @@ import {
|
||||
readMetadataString,
|
||||
sha256Bytes,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import {
|
||||
memberListIndexSize,
|
||||
memberListTable,
|
||||
} from "./ManagedMemberPointers.js";
|
||||
import { memberListTable } from "./ManagedMemberPointers.js";
|
||||
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
|
||||
export type ManagedType = ManagedMemberInspection["types"][number];
|
||||
export type ManagedExportedType = NonNullable<
|
||||
@@ -47,10 +45,10 @@ export type ManagedExceptionRegion =
|
||||
export interface TypeRange {
|
||||
readonly token: string;
|
||||
readonly fullName: string | null;
|
||||
readonly fieldStart: number;
|
||||
readonly fieldEnd: number;
|
||||
readonly methodStart: number;
|
||||
readonly methodEnd: number;
|
||||
readonly fieldStart: number | null;
|
||||
readonly fieldEnd: number | null;
|
||||
readonly methodStart: number | null;
|
||||
readonly methodEnd: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,18 +78,23 @@ export interface ParsedInstruction {
|
||||
|
||||
const rowRange = (
|
||||
table: MetadataTableLayout | undefined,
|
||||
start: number,
|
||||
nextStart: number,
|
||||
start: number | null,
|
||||
end: number | null,
|
||||
): {
|
||||
readonly first: number | null;
|
||||
readonly last: number | null;
|
||||
readonly count: number;
|
||||
} => {
|
||||
const total = table?.rowCount ?? 0;
|
||||
if (total === 0 || start === 0) return { first: null, last: null, count: 0 };
|
||||
const end = Math.min(nextStart === 0 ? total + 1 : nextStart, total + 1);
|
||||
if (start >= end) return { first: null, last: null, count: 0 };
|
||||
return { first: start, last: end - 1, count: end - start };
|
||||
if (start === null || end === null)
|
||||
return { first: null, last: null, count: 0 };
|
||||
const lastExclusive = Math.min(end, total + 1);
|
||||
if (start >= lastExclusive) return { first: null, last: null, count: 0 };
|
||||
return {
|
||||
first: start,
|
||||
last: lastExclusive - 1,
|
||||
count: lastExclusive - start,
|
||||
};
|
||||
};
|
||||
|
||||
export const fullName = (namespace: string, name: string): string =>
|
||||
@@ -102,9 +105,8 @@ const readTypeRange = (
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
issues: ManagedParseIssue[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
): TypeRange => {
|
||||
const methods = memberListTable(layout, 6);
|
||||
const fields = memberListTable(layout, 4);
|
||||
const cursor = metadataRowCursor(bytes, layout, 2, row);
|
||||
cursor.readUInt32();
|
||||
const nameIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
@@ -124,27 +126,14 @@ const readTypeRange = (
|
||||
issues,
|
||||
) ?? null;
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
|
||||
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
|
||||
let nextField = (fields?.rowCount ?? 0) + 1;
|
||||
let nextMethod = (methods?.rowCount ?? 0) + 1;
|
||||
const typeTable = layout.table(2);
|
||||
if (typeTable !== undefined && row < typeTable.rowCount) {
|
||||
const next = metadataRowCursor(bytes, layout, 2, row + 1);
|
||||
next.readUInt32();
|
||||
next.readIndex(layout.stringIndexSize);
|
||||
next.readIndex(layout.stringIndexSize);
|
||||
next.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
nextField = next.readIndex(memberListIndexSize(layout, 4));
|
||||
nextMethod = next.readIndex(memberListIndexSize(layout, 6));
|
||||
}
|
||||
const current = memberLists.entries[row - 1];
|
||||
return {
|
||||
token: metadataToken(2, row),
|
||||
fullName: declaredName,
|
||||
fieldStart,
|
||||
fieldEnd: nextField,
|
||||
methodStart,
|
||||
methodEnd: nextMethod,
|
||||
fieldStart: current?.field.start.value ?? null,
|
||||
fieldEnd: current?.field.end ?? null,
|
||||
methodStart: current?.method.start.value ?? null,
|
||||
methodEnd: current?.method.end ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -152,11 +141,13 @@ export const typeRanges = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
issues: ManagedParseIssue[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
): readonly TypeRange[] => {
|
||||
issues.push(...memberLists.issues);
|
||||
const typeTable = layout.table(2);
|
||||
const ranges: TypeRange[] = [];
|
||||
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1)
|
||||
ranges.push(readTypeRange(bytes, layout, row, issues));
|
||||
ranges.push(readTypeRange(bytes, layout, row, issues, memberLists));
|
||||
return ranges;
|
||||
};
|
||||
|
||||
@@ -193,18 +184,16 @@ export const parseTypes = (
|
||||
const extendsRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("TypeDefOrRef"),
|
||||
);
|
||||
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
|
||||
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
|
||||
const nextRange = ranges[row] ?? null;
|
||||
const currentRange = ranges[row - 1] ?? null;
|
||||
const fieldRange = rowRange(
|
||||
fields,
|
||||
fieldStart,
|
||||
nextRange?.fieldStart ?? 0,
|
||||
currentRange?.fieldStart ?? null,
|
||||
currentRange?.fieldEnd ?? null,
|
||||
);
|
||||
const methodRange = rowRange(
|
||||
methods,
|
||||
methodStart,
|
||||
nextRange?.methodStart ?? 0,
|
||||
currentRange?.methodStart ?? null,
|
||||
currentRange?.methodEnd ?? null,
|
||||
);
|
||||
const extendsReason = metadataCodedTokenInvalidReason(
|
||||
extendsRaw,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
|
||||
import { memberListIndexSize } from "./ManagedMemberPointers.js";
|
||||
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
buildManagedMemberPointerFixture,
|
||||
@@ -28,15 +29,15 @@ const metadataLayout = (bytes: Buffer) => {
|
||||
describe("managed declaring type ownership", () => {
|
||||
it.prop([
|
||||
fc.tuple(
|
||||
fc.tuple(
|
||||
...Array.from({ length: 3 }, () => fc.integer({ min: 0, max: 6 })),
|
||||
),
|
||||
fc.tuple(
|
||||
...Array.from({ length: 3 }, () => fc.integer({ min: 0, max: 6 })),
|
||||
),
|
||||
fc
|
||||
.array(fc.integer({ min: 1, max: 3 }), { minLength: 3, maxLength: 3 })
|
||||
.map((starts) => starts.sort((left, right) => left - right)),
|
||||
fc
|
||||
.array(fc.integer({ min: 1, max: 3 }), { minLength: 3, maxLength: 3 })
|
||||
.map((starts) => starts.sort((left, right) => left - right)),
|
||||
),
|
||||
])(
|
||||
"retains first-match field and method owners for arbitrary metadata lists",
|
||||
"matches literal ownership ranges for valid nondecreasing list starts",
|
||||
([fields, methods]) => {
|
||||
const bytes = buildManagedMemberPointerFixture();
|
||||
const layout = metadataLayout(bytes);
|
||||
@@ -64,11 +65,16 @@ describe("managed declaring type ownership", () => {
|
||||
] as const) {
|
||||
// The fixture's pointers reorder physical rows 1,2 to list positions 2,1.
|
||||
const expected = [2, 1].map((position) => {
|
||||
const owner = starts.findIndex(
|
||||
(start, index) =>
|
||||
position >= start && position < (starts[index + 1] ?? 3),
|
||||
);
|
||||
return owner < 0 ? null : members.types[owner]?.full_name;
|
||||
let owner: number | null = null;
|
||||
for (let index = 0; index < starts.length; index += 1) {
|
||||
const start = starts[index];
|
||||
const end = starts[index + 1] ?? 3;
|
||||
if (start !== undefined && position >= start && position < end) {
|
||||
owner = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return owner === null ? null : members.types[owner]?.full_name;
|
||||
});
|
||||
expect(members[table].map((member) => member.declaring_type)).toEqual(
|
||||
expected,
|
||||
@@ -80,10 +86,10 @@ describe("managed declaring type ownership", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("indexes large unordered member lists once per layout", () => {
|
||||
const count = 100_000;
|
||||
it("indexes wide valid member lists once per layout", () => {
|
||||
const count = 65_538;
|
||||
const bytes = buildManagedPeFixture({
|
||||
additionalEmptyTypes: count - 1,
|
||||
additionalEmptyTypes: count - 3,
|
||||
methodRowCount: count,
|
||||
});
|
||||
const layout = metadataLayout(bytes);
|
||||
@@ -95,24 +101,25 @@ describe("managed declaring type ownership", () => {
|
||||
2 * layout.stringIndexSize +
|
||||
layout.codedIndexSize("TypeDefOrRef") +
|
||||
memberListIndexSize(layout, 4);
|
||||
// The first reversed range forces first-match semantics, with the owner last.
|
||||
// 100,000 MethodDef rows require a four-byte method-list index.
|
||||
bytes.writeUInt32LE(2, methodOffset);
|
||||
const start = performance.now();
|
||||
const lookup = memberDeclaringTypeRowLookup(bytes, layout, 6);
|
||||
expect(
|
||||
Array.from({ length: count }, (_, row) => lookup(row + 1)).every(
|
||||
(owner) => owner === count,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(performance.now() - start).toBeLessThan(2000);
|
||||
expect(memberDeclaringTypeRowLookup(bytes, layout, 6)).toBe(lookup);
|
||||
// Each preceding TypeDef owns one row; later TypeDefs have empty intervals.
|
||||
// This exercises the four-byte list-index boundary without malformed starts.
|
||||
for (let row = 0; row < count; row += 1)
|
||||
bytes.writeUInt32LE(
|
||||
Math.min(row + 1, 65_537),
|
||||
methodOffset + row * types.rowSize,
|
||||
);
|
||||
const facts = readTypeMemberListFacts(bytes, layout);
|
||||
const lookup = memberDeclaringTypeRowLookup(bytes, layout, 6, facts);
|
||||
expect(lookup(65_536)).toBe(65_536);
|
||||
expect(lookup(65_535)).toBe(65_535);
|
||||
expect(memberDeclaringTypeRowLookup(bytes, layout, 6, facts)).toBe(lookup);
|
||||
expect([lookup(0), lookup(count + 1)]).toEqual([0, 0]);
|
||||
});
|
||||
|
||||
it("admits ownership storage before allocation and retains failed admission", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const layout = metadataLayout(bytes);
|
||||
const facts = readTypeMemberListFacts(bytes, layout);
|
||||
const budget = managedDecodeBudget(layout);
|
||||
const capacityFailure = new ManagedReaderFailure({
|
||||
code: "resource-limit",
|
||||
@@ -134,12 +141,12 @@ describe("managed declaring type ownership", () => {
|
||||
}),
|
||||
);
|
||||
try {
|
||||
expect(() => memberDeclaringTypeRowLookup(bytes, layout, 6)).toThrow(
|
||||
capacityFailure,
|
||||
);
|
||||
expect(() => memberDeclaringTypeRowLookup(bytes, layout, 6)).toThrow(
|
||||
capacityFailure,
|
||||
);
|
||||
expect(() =>
|
||||
memberDeclaringTypeRowLookup(bytes, layout, 6, facts),
|
||||
).toThrow(capacityFailure);
|
||||
expect(() =>
|
||||
memberDeclaringTypeRowLookup(bytes, layout, 6, facts),
|
||||
).toThrow(capacityFailure);
|
||||
expect(admission).toHaveBeenCalledTimes(1);
|
||||
expect(allocations).toBe(0);
|
||||
} finally {
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { metadataRowCursor } from "./ManagedMetadataHeaps.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
memberListIndexSize,
|
||||
memberListRowLookup,
|
||||
memberListTable,
|
||||
} from "./ManagedMemberPointers.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import type {
|
||||
MemberDefinitionTable,
|
||||
TypeMemberListFacts,
|
||||
} from "./ManagedTypeMemberLists.js";
|
||||
|
||||
type MemberTable = 4 | 6;
|
||||
type OwnerLookup = (definitionRow: number) => number;
|
||||
const lookups = new WeakMap<
|
||||
ManagedMetadataLayout,
|
||||
Map<MemberTable, OwnerLookup | ManagedReaderFailure>
|
||||
TypeMemberListFacts,
|
||||
Map<MemberDefinitionTable, OwnerLookup | ManagedReaderFailure>
|
||||
>();
|
||||
|
||||
/** One first-match TypeDef owner per physical member, shared by every inspection facet. */
|
||||
export const memberDeclaringTypeRowLookup = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
table: MemberTable,
|
||||
table: MemberDefinitionTable,
|
||||
facts: TypeMemberListFacts,
|
||||
): OwnerLookup => {
|
||||
let byTable = lookups.get(layout);
|
||||
let byTable = lookups.get(facts);
|
||||
const cached = byTable?.get(table);
|
||||
if (cached instanceof ManagedReaderFailure) throw cached;
|
||||
if (cached !== undefined) return cached;
|
||||
if (byTable === undefined) {
|
||||
byTable = new Map();
|
||||
lookups.set(layout, byTable);
|
||||
lookups.set(facts, byTable);
|
||||
}
|
||||
try {
|
||||
const positionOf = memberListRowLookup(bytes, layout, table);
|
||||
@@ -50,30 +52,26 @@ export const memberDeclaringTypeRowLookup = (
|
||||
}
|
||||
return root;
|
||||
};
|
||||
const startForType = (row: number): number => {
|
||||
const cursor = metadataRowCursor(bytes, layout, 2, row);
|
||||
cursor.readUInt32();
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
|
||||
return table === 4
|
||||
? fieldStart
|
||||
: cursor.readIndex(memberListIndexSize(layout, 6));
|
||||
};
|
||||
let start = types === 0 ? 0 : startForType(1);
|
||||
for (let row = 1; row <= types; row += 1) {
|
||||
const end = row === types ? positions + 1 : startForType(row + 1);
|
||||
const limit = Math.min(end, positions + 1);
|
||||
for (
|
||||
let p = unowned(Math.min(Math.max(start, 1), positions + 1));
|
||||
p < limit;
|
||||
p = unowned(p + 1)
|
||||
) {
|
||||
owners[p] = row;
|
||||
next[p] = p + 1;
|
||||
const memberStart =
|
||||
table === 4
|
||||
? facts.entries[row - 1]?.field
|
||||
: facts.entries[row - 1]?.method;
|
||||
const start = memberStart?.start.value ?? null;
|
||||
const end = memberStart?.end ?? null;
|
||||
// A malformed endpoint makes this entire interval uncertain. In
|
||||
// particular, zero must not become row one through owner-side clamping.
|
||||
if (start !== null && end !== null) {
|
||||
const limit = Math.min(end, positions + 1);
|
||||
for (
|
||||
let p = unowned(Math.min(start, positions + 1));
|
||||
p < limit;
|
||||
p = unowned(p + 1)
|
||||
) {
|
||||
owners[p] = row;
|
||||
next[p] = p + 1;
|
||||
}
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
const definitions = layout.table(table)?.rowCount ?? 0;
|
||||
const lookup: OwnerLookup = (definition) =>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import type {
|
||||
ManagedMetadataLayout,
|
||||
MetadataTableLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
managedFailure,
|
||||
ManagedReaderFailure,
|
||||
@@ -32,6 +35,17 @@ export const memberListIndexSize = (
|
||||
? 4
|
||||
: 2;
|
||||
|
||||
/** Normalize a TypeDef list coordinate once for parsing and owner lookup. */
|
||||
export const validatedMemberListStart = (
|
||||
start: number,
|
||||
table: MetadataTableLayout | undefined,
|
||||
): number | null => {
|
||||
const total = table?.rowCount ?? 0;
|
||||
return Number.isSafeInteger(start) && start >= 1 && start <= total + 1
|
||||
? start
|
||||
: null;
|
||||
};
|
||||
|
||||
/** Resolve physical definition rows to list positions without changing token identity. */
|
||||
export const memberListRowLookup = (
|
||||
bytes: Buffer,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { readManagedValue } from "./ManagedReaderFailure.js";
|
||||
import { methodBody } from "./ManagedMethodBodyReader.js";
|
||||
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
|
||||
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
|
||||
/**
|
||||
* Declaring-type tokens whose members are decoded in full; null selects every
|
||||
@@ -47,6 +48,7 @@ export const parseFields = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
ranges: readonly TypeRange[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
selected: SelectedTypeTokens = null,
|
||||
): {
|
||||
readonly fields: readonly ManagedField[];
|
||||
@@ -58,7 +60,7 @@ export const parseFields = (
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const table = layout.table(4);
|
||||
const ownerRow = readManagedValue(
|
||||
() => memberDeclaringTypeRowLookup(bytes, layout, 4),
|
||||
() => memberDeclaringTypeRowLookup(bytes, layout, 4, memberLists),
|
||||
issues,
|
||||
);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
@@ -256,6 +258,7 @@ interface ParseMethodsInput {
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly pe: ManagedPeLayout;
|
||||
readonly ranges: readonly TypeRange[];
|
||||
readonly memberLists: TypeMemberListFacts;
|
||||
readonly selected?: SelectedTypeTokens;
|
||||
readonly methodOffset?: number;
|
||||
readonly methodLimit?: number;
|
||||
@@ -266,6 +269,7 @@ export const parseMethods = ({
|
||||
layout,
|
||||
pe,
|
||||
ranges,
|
||||
memberLists,
|
||||
selected = null,
|
||||
methodOffset = 0,
|
||||
methodLimit = Number.MAX_SAFE_INTEGER,
|
||||
@@ -285,7 +289,7 @@ export const parseMethods = ({
|
||||
let unscannedMethods = 0;
|
||||
const table = layout.table(6);
|
||||
const ownerRow = readManagedValue(
|
||||
() => memberDeclaringTypeRowLookup(bytes, layout, 6),
|
||||
() => memberDeclaringTypeRowLookup(bytes, layout, 6, memberLists),
|
||||
issues,
|
||||
);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
readResource,
|
||||
} from "./ManagedMetadataInventoryRows.js";
|
||||
import { metadataToken } from "./ManagedMetadataHeaps.js";
|
||||
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
import {
|
||||
readManagedValue,
|
||||
uniqueManagedParseIssues,
|
||||
@@ -149,13 +150,22 @@ const readAttributes = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
issues: ManagedParseIssue[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
): {
|
||||
readonly attributes: readonly CustomAttribute[];
|
||||
readonly targetFrameworks: readonly string[];
|
||||
} => {
|
||||
const attributes = readRows(layout.table(12), (row) =>
|
||||
readManagedValue(
|
||||
() => readCustomAttribute(bytes, layout, row, heapExtent(layout), issues),
|
||||
() =>
|
||||
readCustomAttribute(
|
||||
bytes,
|
||||
layout,
|
||||
row,
|
||||
heapExtent(layout),
|
||||
issues,
|
||||
memberLists,
|
||||
),
|
||||
issues,
|
||||
),
|
||||
);
|
||||
@@ -177,8 +187,9 @@ export const readManagedMetadataInventory = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
resourceDirectory: ManagedResourceDirectory | null,
|
||||
memberLists: TypeMemberListFacts,
|
||||
): ManagedMetadataInventory => {
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const issues: ManagedParseIssue[] = [...memberLists.issues];
|
||||
validateIdentityTableCounts(layout, issues);
|
||||
const module =
|
||||
readManagedValue(
|
||||
@@ -201,6 +212,7 @@ export const readManagedMetadataInventory = (
|
||||
bytes,
|
||||
layout,
|
||||
issues,
|
||||
memberLists,
|
||||
);
|
||||
return {
|
||||
module,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
|
||||
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
|
||||
import type {
|
||||
ManagedArtifactInspection,
|
||||
@@ -248,8 +249,14 @@ const declaringTypeForMethod = (
|
||||
layout: ManagedMetadataLayout,
|
||||
methodRow: number,
|
||||
heapExtent: number,
|
||||
memberLists: TypeMemberListFacts,
|
||||
): string | null => {
|
||||
const owner = memberDeclaringTypeRowLookup(bytes, layout, 6)(methodRow);
|
||||
const owner = memberDeclaringTypeRowLookup(
|
||||
bytes,
|
||||
layout,
|
||||
6,
|
||||
memberLists,
|
||||
)(methodRow);
|
||||
if (owner === 0) return null;
|
||||
return readTypeName({ bytes, layout, table: 2, row: owner, heapExtent });
|
||||
};
|
||||
@@ -259,11 +266,13 @@ const attributeTypeName = (
|
||||
layout: ManagedMetadataLayout,
|
||||
rawType: number,
|
||||
heapExtent: number,
|
||||
memberLists: TypeMemberListFacts,
|
||||
): string | null => {
|
||||
const tag = rawType & 7;
|
||||
const row = Math.floor(rawType / 8);
|
||||
if (row === 0) return null;
|
||||
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, heapExtent);
|
||||
if (tag === 2)
|
||||
return declaringTypeForMethod(bytes, layout, row, heapExtent, memberLists);
|
||||
if (tag !== 3) return null;
|
||||
const memberRefs = layout.table(10);
|
||||
if (memberRefs === undefined || row > memberRefs.rowCount) return null;
|
||||
@@ -288,7 +297,13 @@ const attributeTypeName = (
|
||||
heapExtent,
|
||||
});
|
||||
if (parentTag === 3)
|
||||
return declaringTypeForMethod(bytes, layout, parentRow, heapExtent);
|
||||
return declaringTypeForMethod(
|
||||
bytes,
|
||||
layout,
|
||||
parentRow,
|
||||
heapExtent,
|
||||
memberLists,
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -349,6 +364,7 @@ export const readCustomAttribute = (
|
||||
row: number,
|
||||
heapExtent: number,
|
||||
issues: ManagedParseIssue[],
|
||||
memberLists: TypeMemberListFacts,
|
||||
): CustomAttribute => {
|
||||
const cursor = metadataRowCursor(bytes, layout, 12, row);
|
||||
const parentRaw = cursor.readIndex(
|
||||
@@ -411,7 +427,8 @@ export const readCustomAttribute = (
|
||||
if (constructor !== null)
|
||||
typeName =
|
||||
readManagedValue(
|
||||
() => attributeTypeName(bytes, layout, typeRaw, heapExtent),
|
||||
() =>
|
||||
attributeTypeName(bytes, layout, typeRaw, heapExtent, memberLists),
|
||||
issues,
|
||||
) ?? null;
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
readManagedMetadataLayout,
|
||||
type ManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
|
||||
import {
|
||||
readManagedPeLayout,
|
||||
type ManagedPeLayout,
|
||||
@@ -108,10 +109,12 @@ const readBoundaryInventory = (
|
||||
metadataOffset,
|
||||
cli.metadata.size,
|
||||
);
|
||||
const memberLists = readTypeMemberListFacts(bytes, layout);
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
layout,
|
||||
readManagedResourceDirectory(pe, issues),
|
||||
memberLists,
|
||||
);
|
||||
return { layout, inventory };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import { metadataRowCursor, metadataToken } from "./ManagedMetadataHeaps.js";
|
||||
import {
|
||||
memberListIndexSize,
|
||||
memberListTable,
|
||||
validatedMemberListStart,
|
||||
} from "./ManagedMemberPointers.js";
|
||||
|
||||
export type MemberDefinitionTable = 4 | 6;
|
||||
|
||||
export interface TypeMemberListStart {
|
||||
readonly raw: number;
|
||||
readonly value: number | null;
|
||||
readonly offset: number;
|
||||
}
|
||||
|
||||
export interface TypeMemberListRange {
|
||||
readonly start: TypeMemberListStart;
|
||||
/** Next TypeDef's validated start, or the one-past-end sentinel. */
|
||||
readonly end: number | null;
|
||||
}
|
||||
|
||||
export interface TypeMemberListEntry {
|
||||
readonly field: TypeMemberListRange;
|
||||
readonly method: TypeMemberListRange;
|
||||
}
|
||||
|
||||
/** Parsed TypeDef list coordinates for one metadata-layout lifetime. */
|
||||
export interface TypeMemberListFacts {
|
||||
readonly entries: readonly TypeMemberListEntry[];
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
}
|
||||
|
||||
export const readTypeMemberListFacts = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
): TypeMemberListFacts => {
|
||||
const fields = memberListTable(layout, 4);
|
||||
const methods = memberListTable(layout, 6);
|
||||
const rows: { field: TypeMemberListStart; method: TypeMemberListStart }[] =
|
||||
[];
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
const types = layout.table(2);
|
||||
const typeCount = types?.rowCount ?? 0;
|
||||
// Peak retained shape includes both the parsed-start and range arrays, their
|
||||
// nested records, plus each eventual JSON object/key projection. Malformed
|
||||
// row diagnostics are admitted separately before they are allocated.
|
||||
managedDecodeBudget(layout).reserve(
|
||||
typeCount * 512 + 256,
|
||||
types?.offset ?? null,
|
||||
);
|
||||
for (let row = 1; row <= typeCount; row += 1) {
|
||||
const cursor = metadataRowCursor(bytes, layout, 2, row);
|
||||
cursor.readUInt32();
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
const field = {
|
||||
offset: cursor.offset,
|
||||
raw: cursor.readIndex(memberListIndexSize(layout, 4)),
|
||||
};
|
||||
const method = {
|
||||
offset: cursor.offset,
|
||||
raw: cursor.readIndex(memberListIndexSize(layout, 6)),
|
||||
};
|
||||
const entry = {
|
||||
field: { ...field, value: validatedMemberListStart(field.raw, fields) },
|
||||
method: {
|
||||
...method,
|
||||
value: validatedMemberListStart(method.raw, methods),
|
||||
},
|
||||
};
|
||||
rows.push(entry);
|
||||
for (const [name, start, table] of [
|
||||
["FieldList", entry.field, fields],
|
||||
["MethodList", entry.method, methods],
|
||||
] as const)
|
||||
if (start.value === null) {
|
||||
managedDecodeBudget(layout).reserve(512, start.offset);
|
||||
issues.push({
|
||||
code: "invalid-row",
|
||||
scope: `metadata.TypeDef:${metadataToken(2, row)}`,
|
||||
offset: start.offset,
|
||||
detail: `TypeDef ${name} start ${String(start.raw)} is outside the one-based list range 1..${String((table?.rowCount ?? 0) + 1)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const nonmonotonicFields = new Set<number>();
|
||||
const nonmonotonicMethods = new Set<number>();
|
||||
for (let index = 0; index + 1 < rows.length; index += 1) {
|
||||
const current = rows[index];
|
||||
const next = rows[index + 1];
|
||||
if (current === undefined || next === undefined) continue;
|
||||
for (const [name, before, after, affected] of [
|
||||
["FieldList", current.field, next.field, nonmonotonicFields],
|
||||
["MethodList", current.method, next.method, nonmonotonicMethods],
|
||||
] as const)
|
||||
if (
|
||||
before.value !== null &&
|
||||
after.value !== null &&
|
||||
after.value < before.value
|
||||
) {
|
||||
managedDecodeBudget(layout).reserve(512, after.offset);
|
||||
affected.add(index);
|
||||
affected.add(index + 1);
|
||||
issues.push({
|
||||
code: "invalid-row",
|
||||
scope: `metadata.TypeDef:${metadataToken(2, index + 2)}`,
|
||||
offset: after.offset,
|
||||
detail: `TypeDef ${name} start ${String(after.raw)} decreases from preceding start ${String(before.raw)}; adjacent member ranges are unknown`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const entries: TypeMemberListEntry[] = rows.map((entry, index) => {
|
||||
const next = rows[index + 1];
|
||||
return {
|
||||
field: {
|
||||
start: {
|
||||
...entry.field,
|
||||
value: nonmonotonicFields.has(index) ? null : entry.field.value,
|
||||
},
|
||||
end:
|
||||
next === undefined
|
||||
? (fields?.rowCount ?? 0) + 1
|
||||
: nonmonotonicFields.has(index) || nonmonotonicFields.has(index + 1)
|
||||
? null
|
||||
: next.field.value,
|
||||
},
|
||||
method: {
|
||||
start: {
|
||||
...entry.method,
|
||||
value: nonmonotonicMethods.has(index) ? null : entry.method.value,
|
||||
},
|
||||
end:
|
||||
next === undefined
|
||||
? (methods?.rowCount ?? 0) + 1
|
||||
: nonmonotonicMethods.has(index) ||
|
||||
nonmonotonicMethods.has(index + 1)
|
||||
? null
|
||||
: next.method.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { entries, issues };
|
||||
};
|
||||
@@ -12,6 +12,75 @@ import {
|
||||
managedPeFixtureTarget,
|
||||
} from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
describe("managed member structural matching", () => {
|
||||
it("keeps unequal constants from becoming a structural remap", () => {
|
||||
const observe = (
|
||||
typeName: string,
|
||||
methodName: string,
|
||||
constant: number,
|
||||
branchOffset: number,
|
||||
) =>
|
||||
inspect(
|
||||
buildManagedPeFixture({
|
||||
typeName,
|
||||
methodName,
|
||||
ilBody: Buffer.from([
|
||||
0x1e,
|
||||
0x1f,
|
||||
constant,
|
||||
0x26,
|
||||
0x2b,
|
||||
branchOffset,
|
||||
0x00,
|
||||
0x2a,
|
||||
]),
|
||||
}),
|
||||
`/tmp/${typeName}-${constant}.dll`,
|
||||
);
|
||||
const left = observe("OldType", "OldMethod", 5, 1);
|
||||
const differentConstant = observe("NewType", "NewMethod", 7, 1);
|
||||
const sameConstant = observe("NewType", "NewMethod", 5, 0);
|
||||
for (const [observed, constant] of [
|
||||
[left, 5],
|
||||
[differentConstant, 7],
|
||||
[sameConstant, 5],
|
||||
] as const) {
|
||||
expect(observed.result.methods[0]?.body).toMatchObject({
|
||||
status: "present",
|
||||
normalized_il_sha256: expect.any(String),
|
||||
anchors: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
operand_kind: "constant",
|
||||
operand: String(constant),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
const different = compareManagedMembers(left, differentConstant);
|
||||
expect(different.methods.map(({ status }) => status)).toEqual([
|
||||
"removed",
|
||||
"added",
|
||||
]);
|
||||
expect(
|
||||
different.methods.some(
|
||||
({ match }) => match.basis === "structural-method-shape",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
expect(compareManagedMembers(left, sameConstant).methods).toContainEqual(
|
||||
expect.objectContaining({
|
||||
status: "changed",
|
||||
match: expect.objectContaining({
|
||||
status: "matched",
|
||||
basis: "structural-method-shape",
|
||||
confidence: "high",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member comparison", () => {
|
||||
it("remaps renamed methods by exact CIL/signature without using names", () => {
|
||||
const leftBytes = buildManagedPeFixture();
|
||||
|
||||
@@ -16,21 +16,27 @@ const exampleInput = () =>
|
||||
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
|
||||
);
|
||||
|
||||
const nativeEvidenceWithExports = (names: readonly string[], path?: string) => {
|
||||
const nativeEvidenceWithExports = (
|
||||
names: readonly string[],
|
||||
path?: string,
|
||||
subjectless = false,
|
||||
) => {
|
||||
const input = exampleInput();
|
||||
const native = input.native_observations[0];
|
||||
if (native === undefined || native.subject === null)
|
||||
throw new Error("missing native example");
|
||||
const macho = inspectMachoSchema.parse(native.normalized_result);
|
||||
return createEvidence(
|
||||
{
|
||||
path: path ?? native.subject.local_path,
|
||||
sha256: native.subject.digest.sha256,
|
||||
format: native.subject.format,
|
||||
...(native.subject.architecture === null
|
||||
? {}
|
||||
: { architecture: native.subject.architecture }),
|
||||
},
|
||||
subjectless
|
||||
? undefined
|
||||
: {
|
||||
path: path ?? native.subject.local_path,
|
||||
sha256: native.subject.digest.sha256,
|
||||
format: native.subject.format,
|
||||
...(native.subject.architecture === null
|
||||
? {}
|
||||
: { architecture: native.subject.architecture }),
|
||||
},
|
||||
native.provider,
|
||||
{
|
||||
operation: native.operation,
|
||||
@@ -60,7 +66,7 @@ const nativeEvidenceWithExports = (names: readonly string[], path?: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
const inputWithImportScope = (scope: string) => {
|
||||
const inputWithImportScope = (scope: string | null) => {
|
||||
const input = exampleInput();
|
||||
const managed = input.managed_boundaries;
|
||||
const normalized = managedNativeBoundaryInspectionSchema.parse(
|
||||
@@ -226,6 +232,41 @@ it("keeps literal module names distinct and accepts the shared-library lib alias
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps name candidates unresolved when a required module identity is absent", () => {
|
||||
for (const scope of [null, "", " "]) {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...inputWithImportScope(scope),
|
||||
native_observations: [nativeEvidenceWithExports(["open_native"])],
|
||||
});
|
||||
expect(result.pinvoke_imports[0]).toMatchObject({
|
||||
status: "unresolved",
|
||||
basis: "module-identity-unknown",
|
||||
matched_native: { name: "open_native" },
|
||||
candidates: [{ name: "open_native" }],
|
||||
});
|
||||
expect(result.pinvoke_imports[0]?.limitations).toContain(
|
||||
"A native name candidate exists, but a required module identity is unavailable; module correspondence is unknown.",
|
||||
);
|
||||
}
|
||||
|
||||
const subjectless = verifyManagedNativeBoundaries({
|
||||
...inputWithImportScope("nativehelper.dll"),
|
||||
native_observations: [
|
||||
nativeEvidenceWithExports(["open_native"], undefined, true),
|
||||
],
|
||||
});
|
||||
expect(subjectless.pinvoke_imports[0]).toMatchObject({
|
||||
status: "unresolved",
|
||||
basis: "module-identity-unknown",
|
||||
matched_native: {
|
||||
name: "open_native",
|
||||
module_name: null,
|
||||
module_path: null,
|
||||
},
|
||||
candidates: [{ name: "open_native" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("treats supported native Evidence with no symbols as unresolved", () => {
|
||||
const result = verifyManagedNativeBoundaries({
|
||||
...exampleInput(),
|
||||
|
||||
Reference in New Issue
Block a user