Files
rea/src/dotnet/ManagedMemberInspector.test.ts
T
skoodsterGitHubskaredClaude Opus 5.5Ysopwnablefactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>Kaoru0822
a149d183ef feat(managed): select declaring types in inspect_managed_members
Select exact declaring types before managed signatures and CIL bodies are decoded, preserving caller choices in CLI/MCP Evidence. Keep unmatched names and selected coverage explicit; comparisons and graphs retain unknowns outside their observed scope. Reuse existing metadata ownership/accounting and advertise unique selections through the actual SDK Schema.

Validated with 229 focused tests across 21 files, 11 managed conformance controls, fullstatic/docs checks and real Linux CLI and fresh MCP inspection of two unchanged DLLs. All 118 selected CIL hashes matched original PE-mapped bytes; result/caller-argument parity and unmatched controls passed. Full artifact/native-boundary metadata is complete; whole-assembly members retain the unchanged 64 MiB resource constraint. No target code executes.

Refs #1878

Co-authored-by: skared <190618464+skoodster@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-12 08:31:37 +08:00

927 lines
29 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
import { edges } from "./ManagedMemberRows.js";
import { signature, type ManagedMethod } from "./ManagedMemberInspectorCore.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import { managedMemberInspectionSchema } from "../domain/managed/managedArtifact.js";
import { comparisonLimitations } from "../domain/managed/managedMemberComparisonCoverage.js";
import {
buildManagedPeFixture,
buildNativePeFixture,
managedPeFixtureTarget,
} from "./ManagedPe.fixture.js";
describe("managed member inspection", () => {
it("keeps metadata handles distinct from calls and field accesses", () => {
const il = Buffer.from([
0xd0, 0x01, 0, 0, 0x06, 0x26, 0xd0, 0x01, 0, 0, 0x04, 0x26, 0x28, 0x01, 0,
0, 0x0a, 0x7b, 0x01, 0, 0, 0x04, 0x26, 0x2a,
]);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.body.anchors.map(({ opcode }) => opcode)).toEqual(
["ldtoken", "ldtoken", "call", "ldfld"],
);
expect(result.call_edges.map(({ opcode }) => opcode)).toEqual(["call"]);
expect(result.field_accesses.map(({ opcode }) => opcode)).toEqual([
"ldfld",
]);
});
it("inspects metadata members, signatures, CIL hashes, call edges, and field anchors", () => {
const bytes = buildManagedPeFixture();
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.identity_scope).toEqual({
token_identity: "build-local",
requires_artifact_sha256: managedPeFixtureTarget(bytes).sha256,
requires_mvid: "00112233-4455-6677-8899-aabbccddeeff",
});
expect(result.types).toEqual([
expect.objectContaining({
token: "0x02000001",
full_name: "Fixture.Program",
field_list: { first_row: 1, last_row: 1, count: 1 },
method_list: { first_row: 1, last_row: 1, count: 1 },
}),
]);
expect(result.fields).toEqual([
expect.objectContaining({
token: "0x04000001",
declaring_type: "Fixture.Program",
name: "counter",
signature: expect.objectContaining({
kind: "field",
parse_status: "decoded",
field_type: "i4",
}),
}),
]);
expect(result.methods).toEqual([
expect.objectContaining({
token: "0x06000001",
declaring_type: "Fixture.Program",
name: "Main",
rva: 0x2800,
signature: expect.objectContaining({
kind: "method",
parse_status: "decoded",
return_type: "void",
parameter_types: [],
}),
body: expect.objectContaining({
status: "present",
header_format: "tiny",
file_offset: 0x0a00,
il_size: 12,
instruction_count: 4,
decoded_instruction_count: 4,
truncated_instructions: 0,
opcode_counts: { "ldarg.0": 1, ldfld: 1, call: 1, ret: 1 },
anchors: [
{
il_offset: 1,
opcode: "ldfld",
operand_kind: "field",
operand: "0x04000001",
},
{
il_offset: 6,
opcode: "call",
operand_kind: "method",
operand: "0x0a000001",
},
],
}),
}),
]);
expect(result.member_refs).toEqual([
expect.objectContaining({
token: "0x0a000001",
name: ".ctor",
signature: expect.objectContaining({
kind: "method",
parse_status: "decoded",
parameter_types: ["string"],
}),
}),
]);
expect(result.call_edges).toEqual([
{
caller_token: "0x06000001",
caller: "Fixture.Program.Main",
opcode: "call",
target_token: "0x0a000001",
target_kind: "member-ref",
target_name: ".ctor",
},
]);
expect(result.field_accesses).toEqual([
{
method_token: "0x06000001",
method: "Fixture.Program.Main",
opcode: "ldfld",
field_token: "0x04000001",
field_name: "counter",
},
]);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
describe("managed exported types", () => {
it("reports type forwarders and nested exported types with their implementation scope", () => {
const bytes = buildManagedPeFixture({
exportedTypes: [
{
namespace: "System",
name: "Object",
flags: 0x0020_0001,
implementationRaw: (1 << 2) | 1,
},
{
namespace: "",
name: "Enumerator",
flags: 0x0000_0002,
implementationRaw: (1 << 2) | 2,
},
],
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.metadata.table_row_counts).toMatchObject({
ExportedType: 2,
});
expect(result.exported_types).toMatchObject([
{
token: "0x27000001",
namespace: "System",
name: "Object",
flags: 0x0020_0001,
forwarder: true,
type_def_id: 0,
implementation_token: "0x23000001",
},
{
token: "0x27000002",
namespace: "",
name: "Enumerator",
forwarder: false,
implementation_token: "0x27000001",
},
]);
expect(result.coverage).toEqual({ state: "complete", issues: [] });
});
it("leaves exported types unknown when CLI metadata is unavailable", () => {
const bytes = buildNativePeFixture();
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage.state).toBe("unavailable");
expect(result.exported_types).toBeNull();
});
it("reads member Evidence recorded before exported types as unexamined", () => {
const bytes = buildManagedPeFixture();
const { exported_types: _omitted, ...legacy } = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const parsed = managedMemberInspectionSchema.parse(legacy);
expect(parsed.exported_types).toBeNull();
expect(comparisonLimitations(parsed, parsed)).toContain(
"At least one managed member observation predates ExportedType inspection; its exported and forwarded types are unknown.",
);
});
it("retains an exported type whose implementation is out of range as partial", () => {
const bytes = buildManagedPeFixture({
exportedTypes: [
{
namespace: "System",
name: "Object",
flags: 0x0020_0001,
implementationRaw: (9 << 2) | 1,
},
],
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.exported_types).toMatchObject([
{ token: "0x27000001", name: "Object", implementation_token: null },
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.ExportedType:0x27000001",
detail: expect.stringContaining(
"ExportedType implementation coded index 0x25 is invalid",
),
},
],
});
});
});
describe("managed member decoding budget", () => {
it("admits a wide-instruction method whose actual records fit the budget", () => {
// Five-byte branches all target the next instruction. A final ret leaves
// the method stack-neutral, despite over 256 KiB of valid IL bytes.
const il = Buffer.alloc(270_001);
for (let offset = 0; offset < il.length - 1; offset += 5) {
il[offset] = 0x38;
}
il[il.length - 1] = 0x2a;
const header = Buffer.alloc(12);
header.writeUInt16LE(0x3003, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
expect(result.types).toHaveLength(1);
expect(result.methods[0]?.body).toMatchObject({
status: "present",
il_size: il.length,
decoded_instruction_count: 54_001,
opcode_counts: { br: 54_000, ret: 1 },
});
});
it("still rejects dense one-byte instructions before exceeding capacity", () => {
// All NOPs: unlike wide instructions, each IL byte adds a decoded record.
// A true oversized transient must remain an actionable resource limit.
const il = Buffer.alloc(270_001);
il[il.length - 1] = 0x2a;
const header = Buffer.alloc(12);
header.writeUInt16LE(0x3003, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({
state: "unavailable",
issues: [{ code: "resource-limit", scope: "metadata.representation" }],
});
// Report the originating IL coordinate, not a generic decoding error.
expect(result.coverage.issues[0]?.offset).toBeGreaterThan(0);
expect(result.types).toEqual([]);
expect(result.methods).toEqual([]);
});
it("charges decoded output rather than accumulated decoding bounds", () => {
// Each bound fits alone, but 256 bytes per IL and signature byte together
// exceed the 64 MiB budget although the retained members are small.
const il = Buffer.concat([Buffer.alloc(200_000), Buffer.from([0x2a])]);
const header = Buffer.alloc(12);
header.writeUInt16LE(0x3003, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il]),
methodSignature: Buffer.concat([
Buffer.from([0x00, 0x00, 0x01]),
Buffer.alloc(80_000),
]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage.state).not.toBe("unavailable");
expect(result.methods[0]?.body).toMatchObject({
status: "present",
il_size: il.length,
opcode_counts: { nop: 200_000, ret: 1 },
});
});
it("bounds a method's IL and exception sections together", () => {
// Each bound fits alone; both stay live while one method is decoded.
const il = Buffer.concat([Buffer.alloc(159_999), Buffer.from([0x2a])]);
const header = Buffer.alloc(12);
header.writeUInt16LE(0x300b, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const clauses = 110_000;
const section = Buffer.alloc(4 + clauses * 24);
section.writeUInt32LE(0x41 | ((4 + clauses * 24) << 8), 0);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il, section]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.coverage).toMatchObject({
state: "unavailable",
issues: [{ code: "resource-limit" }],
});
});
});
describe("managed derived edges", () => {
it("admits call edges before the derived graph exceeds the budget", () => {
const anchor = {
il_offset: 0,
opcode: "call",
operand_kind: "method",
operand: "0x0a000001",
};
const method = {
token: "0x06000001",
declaring_type: "T".repeat(2000),
name: "m",
body: { anchors: Array.from({ length: 40_000 }, () => anchor) },
} as unknown as ManagedMethod;
expect(() =>
edges(
[method],
new Map(),
new Map(),
new Map(),
new ManagedDecodeBudget(),
),
).toThrow(ManagedReaderFailure);
});
});
describe("managed derived caller names", () => {
it("admits a caller name before building it", () => {
let built = false;
// A string-like type name records whether the caller was interpolated.
const declaringType = {
length: 70 * 1024 * 1024,
charCodeAt: () => 0x54,
toString: () => {
built = true;
return "T";
},
} as unknown as string;
const method = {
token: "0x06000001",
declaring_type: declaringType,
name: "m",
body: {
anchors: [
{
il_offset: 0,
opcode: "call",
operand_kind: "method",
operand: "0x0a000001",
},
],
},
} as unknown as ManagedMethod;
expect(() =>
edges(
[method],
new Map(),
new Map(),
new Map(),
new ManagedDecodeBudget(),
),
).toThrow(ManagedReaderFailure);
expect(built).toBe(false);
});
});
describe("managed member reference bounds", () => {
it("retains members and reports an out-of-range TypeDef coded index as partial", () => {
const bytes = buildManagedPeFixture({ extendsRaw: 12 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.types[0]).toMatchObject({
token: "0x02000001",
extends_token: null,
});
expect(result.fields).toHaveLength(1);
expect(result.methods).toHaveLength(1);
expect(result.metadata.status).toBe("partial");
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.TypeDef:0x02000001",
detail: expect.stringContaining(
"TypeDef Extends coded index 0xc is invalid: coded index selects row 3 in table 2, which has 1 rows",
),
},
],
});
expect(result.coverage.issues[0]?.offset).toBe(
(result.types[0]?.row_offset ?? 0) + 8,
);
});
it("retains a MemberRef with an out-of-range parent as an unknown partial reference", () => {
const bytes = buildManagedPeFixture({ memberRefParentRaw: 40 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs[0]).toMatchObject({
token: "0x0a000001",
parent_token: null,
name: ".ctor",
});
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
{
code: "invalid-row",
scope: "metadata.MemberRef:0x0a000001",
detail: expect.stringContaining(
"MemberRef parent coded index 0x28 is invalid: coded index selects row 5 in table 2, which has 1 rows",
),
},
],
});
expect(result.coverage.issues[0]?.offset).toBe(
result.member_refs[0]?.row_offset,
);
});
it("reports a null required MemberRef parent without dropping the reference", () => {
const bytes = buildManagedPeFixture({ memberRefParentRaw: 0 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.member_refs[0]).toMatchObject({
token: "0x0a000001",
parent_token: null,
});
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
detail:
"MemberRef parent coded index 0x0 is null, but the Class column must reference a row",
}),
],
});
});
it("decodes deeply nested supported pointer signatures without recursion", () => {
const depth = 8_000;
const bytes = buildManagedPeFixture({
fieldSignature: Buffer.concat([
Buffer.from([0x06]),
Buffer.alloc(depth, 0x0f),
Buffer.from([0x08]),
]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.fields[0]?.signature).toMatchObject({
kind: "field",
parse_status: "decoded",
field_type: `${"i4"}${"*".repeat(depth)}`,
issue: null,
});
expect(result.methods[0]?.body.status).toBe("present");
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
it.each([
Buffer.alloc(0),
Buffer.from([0]),
Buffer.from([0, 0, 0x12]),
Buffer.from([0x10, 0x80]),
Buffer.from([0, 0xe0]),
])(
"marks incomplete or reserved method signatures malformed: %j",
(methodSignature) => {
const bytes = buildManagedPeFixture({ methodSignature });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.signature).toMatchObject({
parse_status: "malformed",
kind: "unknown",
});
},
);
it("decodes generic instantiation member signatures as complete coverage", () => {
// System.Runtime.Caching 0x06000001: instance void (Dictionary<string, object>)
// shape, with TypeRef/TypeSpec coded tokens kept as encoded.
const bytes = buildManagedPeFixture({
methodSignature: Buffer.from([
0x00, 0x01, 0x01, 0x15, 0x12, 0x71, 0x01, 0x0e,
]),
fieldSignature: Buffer.from([
0x06, 0x15, 0x11, 0x0a, 0x02, 0x13, 0x00, 0x1e, 0x01,
]),
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.signature).toMatchObject({
parse_status: "decoded",
return_type: "void",
parameter_types: ["class:113<string>"],
issue: null,
});
expect(result.fields[0]?.signature).toMatchObject({
parse_status: "decoded",
field_type: "valuetype:10<var:0,mvar:1>",
});
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
it.each([
["single-dimension general array", [0x14, 0x08, 0x01, 0x00, 0x00], "i4[...]"],
[
"sized array with signed lower bounds",
[0x14, 0x08, 0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x7f],
"i4[1...4,-1...1]",
],
[
"partially bounded array",
[0x14, 0x0e, 0x03, 0x01, 0x05, 0x00],
"string[5,,]",
],
[
"function pointer",
[0x1b, 0x00, 0x02, 0x08, 0x0e, 0x1c],
"fnptr[default](string,object)->i4",
],
["required modifier", [0x1f, 0x09, 0x08], "i4 modreq:9"],
[
"optional modifier under a pointer",
[0x0f, 0x20, 0x0d, 0x08],
"i4 modopt:13*",
],
[
"adjacent modifiers in source order",
[0x1f, 0x05, 0x20, 0x09, 0x08],
"i4 modreq:5 modopt:9",
],
[
"modifier runs separated by a pointer",
[0x1f, 0x05, 0x0f, 0x20, 0x09, 0x08],
"i4 modopt:9* modreq:5",
],
[
"unmanaged cdecl function pointer",
[0x1b, 0x01, 0x01, 0x01, 0x18],
"fnptr[c](native-int)->void",
],
[
"array of generic instantiations",
[0x1d, 0x15, 0x12, 0x05, 0x01, 0x14, 0x08, 0x01, 0x01, 0x03, 0x00],
"class:5<i4[3]>[]",
],
])("decodes a %s field signature", (_name, type, expected) => {
expect(signature(Buffer.from([0x06, ...type]))).toMatchObject({
kind: "field",
parse_status: "decoded",
field_type: expected,
issue: null,
});
});
it.each([
["generic instantiation without arguments", [0x15, 0x12, 0x05, 0x00]],
["generic instantiation of a primitive", [0x15, 0x08, 0x05, 0x01, 0x08]],
["zero-rank array", [0x14, 0x08, 0x00, 0x00, 0x00]],
[
"array with more sizes than dimensions",
[0x14, 0x08, 0x01, 0x02, 0x01, 0x01, 0x00],
],
["truncated generic argument list", [0x15, 0x12, 0x05, 0x02, 0x08]],
["truncated function pointer", [0x1b, 0x00, 0x01, 0x08]],
["function pointer with a field header", [0x1b, 0x06, 0x00, 0x01]],
["function pointer with a reserved flag", [0x1b, 0x80, 0x00, 0x01]],
[
"function pointer with explicit-this but no has-this",
[0x1b, 0x40, 0x00, 0x01],
],
["typed reference field", [0x16]],
["modified typed reference field", [0x20, 0x09, 0x16]],
["typed reference generic argument", [0x15, 0x12, 0x05, 0x01, 0x16]],
["typed reference array element", [0x1d, 0x16]],
["by-reference generic argument", [0x15, 0x12, 0x05, 0x01, 0x10, 0x08]],
["void generic argument", [0x15, 0x12, 0x05, 0x01, 0x01]],
["void function pointer parameter", [0x1b, 0x00, 0x01, 0x01, 0x01]],
["by-reference pointee", [0x0f, 0x10, 0x08]],
["nested by-reference", [0x10, 0x10, 0x08]],
["void field", [0x01]],
["void array element", [0x1d, 0x01]],
])("reports a %s as malformed", (_name, type) => {
expect(signature(Buffer.from([0x06, ...type]))).toMatchObject({
kind: "unknown",
parse_status: "malformed",
});
});
it("rejects array ranks beyond the CLR limit before rendering dimensions", () => {
expect(
signature(
Buffer.from([0x06, 0x14, 0x08, 0xdf, 0xff, 0xff, 0xff, 0x00, 0x00]),
),
).toMatchObject({
parse_status: "unsupported",
issue: "array rank 536870911 exceeds the 32 dimensions the CLR supports",
});
expect(
signature(Buffer.from([0x06, 0x14, 0x08, 0x20, 0x00, 0x00])),
).toMatchObject({
parse_status: "decoded",
field_type: `i4[${",".repeat(31)}]`,
});
});
it("decodes typed references in parameter and return positions", () => {
expect(
signature(Buffer.from([0x00, 0x02, 0x16, 0x20, 0x09, 0x16, 0x08])),
).toMatchObject({
kind: "method",
parse_status: "decoded",
return_type: "typedbyref",
parameter_types: ["typedbyref modopt:9", "i4"],
});
expect(
signature(Buffer.from([0x06, 0x1b, 0x20, 0x01, 0x01, 0x16])),
).toMatchObject({
parse_status: "decoded",
field_type: "fnptr[default has-this](typedbyref)->void",
});
});
it.each([
["by-reference field", [0x06, 0x10, 0x08], { field_type: "i4&" }],
["void pointer field", [0x06, 0x0f, 0x01], { field_type: "void*" }],
[
"by-reference function pointer return and parameter",
[0x06, 0x1b, 0x00, 0x01, 0x10, 0x08, 0x10, 0x0e],
{ field_type: "fnptr[default](string&)->i4&" },
],
[
"by-reference method return and parameter",
[0x00, 0x01, 0x10, 0x08, 0x10, 0x15, 0x12, 0x05, 0x01, 0x0f, 0x01],
{ return_type: "i4&", parameter_types: ["class:5<void*>&"] },
],
])("decodes a %s in its grammar position", (_name, blob, expected) => {
expect(signature(Buffer.from(blob))).toMatchObject({
parse_status: "decoded",
...expected,
});
});
it("keeps element types that member signatures do not assign unsupported", () => {
expect(signature(Buffer.from([0x06, 0x45, 0x08]))).toMatchObject({
parse_status: "unsupported",
issue: "unsupported element type 0x45",
});
});
it("decodes deeply nested generic instantiations without recursion", () => {
const depth = 4_000;
const decoded = signature(
Buffer.concat([
Buffer.from([0x06]),
Buffer.from(
Array.from({ length: depth }, () => [0x15, 0x12, 0x05, 0x01]).flat(),
),
Buffer.from([0x08]),
]),
);
expect(decoded).toMatchObject({
parse_status: "decoded",
field_type: `${"class:5<".repeat(depth)}i4${">".repeat(depth)}`,
});
});
it("preserves leading U+FEFF as metadata name content", () => {
const bytes = buildManagedPeFixture({
typeNamespace: "\uFEFFFixture",
typeName: "\uFEFFProgram",
methodName: "\uFEFFMain",
fieldName: "\uFEFFcounter",
});
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.types[0]?.full_name).toBe("\uFEFFFixture.\uFEFFProgram");
expect(result.methods[0]?.name).toBe("\uFEFFMain");
expect(result.fields[0]?.name).toBe("\uFEFFcounter");
expect(result.call_edges[0]?.caller).toBe(
"\uFEFFFixture.\uFEFFProgram.\uFEFFMain",
);
});
it.each([
{ implFlags: 1, flags: 0, label: "native" },
{ implFlags: 3, flags: 0, label: "runtime" },
{ implFlags: 4, flags: 0, label: "unmanaged" },
{ implFlags: 0, flags: 0x2000, label: "pinvoke" },
])(
"does not interpret $label implementations as CIL",
({ implFlags, flags }) => {
const bytes = buildManagedPeFixture();
const original = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const row = original.methods[0]?.row_offset;
expect(row).toBeDefined();
if (row === undefined) return;
bytes.writeUInt16LE(implFlags, row + 4);
bytes.writeUInt16LE(bytes.readUInt16LE(row + 6) | flags, row + 6);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.body).toMatchObject({
status: "partial",
header_format: "unknown",
rva: original.methods[0]?.rva,
il_sha256: null,
normalized_il_sha256: null,
anchors: [],
});
expect(result.methods[0]?.body.issue).toMatch(/CIL/u);
expect(result.coverage.state).toBe("partial");
expect(result.call_edges).toEqual([]);
expect(result.field_accesses).toEqual([]);
},
);
it.each([
{ facet: "fields", nameOffset: 2 },
{ facet: "methods", nameOffset: 8 },
{ facet: "member_refs", nameOffset: 2 },
{ facet: "types", nameOffset: 4 },
] as const)(
"preserves independent metadata when a $facet name is unreadable",
({ facet, nameOffset }) => {
const bytes = buildManagedPeFixture();
const original = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const row = original[facet][0]?.row_offset;
if (row === undefined) throw new Error(`Fixture must contain ${facet}`);
bytes.writeUInt16LE(0xffff, row + nameOffset);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.module?.mvid).toBe(original.module?.mvid);
expect(result.metadata.status).toBe("partial");
expect(result[facet]).toEqual([]);
for (const remaining of [
"fields",
"methods",
"member_refs",
"types",
] as const)
if (remaining !== facet)
expect(result[remaining]).toHaveLength(original[remaining].length);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-heap-index",
scope: "metadata.#Strings",
}),
],
});
if (facet === "types") {
expect(result.fields[0]).toMatchObject({
declaring_type_token: "0x02000001",
declaring_type: null,
});
expect(result.methods[0]).toMatchObject({
declaring_type_token: "0x02000001",
declaring_type: null,
});
}
},
);
describe("managed member type selection", () => {
const bytes = buildManagedPeFixture();
const target = managedPeFixtureTarget(bytes);
const everything = inspectManagedMembersBytes(bytes, target);
it("reports no selection when every type is inspected", () => {
expect(everything.selection).toBeNull();
});
it("decodes the selected type's members and reports unmatched names", () => {
const result = inspectManagedMembersBytes(bytes, target, {
typeNames: ["Fixture.Program", "Missing.Type"],
});
expect(result.selection).toEqual({
type_names: ["Fixture.Program", "Missing.Type"],
unmatched_type_names: ["Missing.Type"],
});
expect(result.types).toEqual(everything.types);
expect(result.fields).toEqual(everything.fields);
expect(result.methods).toEqual(everything.methods);
expect(result.call_edges).toEqual(everything.call_edges);
expect(result.field_accesses).toEqual(everything.field_accesses);
expect(result.coverage).toEqual(everything.coverage);
expect(result.limitations).toContain(
"Types, fields, methods, call edges, and field accesses cover only the selected declaring types; coverage describes that selection. Edge targets outside it keep their names but are not described.",
);
});
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"] },
);
expect(result.selection?.unmatched_type_names).toEqual(["Fixture.Program"]);
expect(result.coverage.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, {
typeNames: ["Missing.Type"],
});
expect(result.selection).toEqual({
type_names: ["Missing.Type"],
unmatched_type_names: ["Missing.Type"],
});
expect(result.types).toEqual([]);
expect(result.fields).toEqual([]);
expect(result.methods).toEqual([]);
expect(result.call_edges).toEqual([]);
expect(result.field_accesses).toEqual([]);
expect(result.member_refs).toEqual(everything.member_refs);
});
});