fix(managed): decode generic, array, fnptr, and modifier signatures (#1835)
* fix(managed): decode generic, array, fnptr, and modifier signatures Member signatures using GENERICINST, ARRAY, FNPTR, CMOD_REQD/CMOD_OPT, or TYPEDBYREF were reported as unsupported, leaving their types unknown and turning otherwise complete inspections partial. Decode them with an explicit stack so nested signatures stay recursion-free, keep encoded TypeDefOrRefOrSpec values, and report malformed shapes as malformed. Fixes #1832 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(managed): bound array rank before rendering dimensions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(managed): keep modifier order and reject non-method fnptr headers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(managed): reject corrupt fnptr flags and misplaced typed references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(managed): decode signature elements by grammar position Track whether each element is a field type, return type, parameter, nested Type, or pointee. VOID is valid only as a return type or pointee, TYPEDBYREF only as a return type or parameter, and BYREF only at the top of a return type, parameter, or ref field; other placements are malformed. Function pointer items are parsed as a return type then parameters, and generic arguments as Types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
069308bbe6
commit
1f531fc257
4 files changed
+478
-67
No files matched your search
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { ManagedDecodeBudget } from "./ManagedDecodeBudget.js";
|
||||
import { edges } from "./ManagedMemberRows.js";
|
||||
import type { ManagedMethod } from "./ManagedMemberInspectorCore.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";
|
||||
@@ -499,18 +499,197 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps an admitted but unimplemented element type unsupported", () => {
|
||||
// ECMA-335 ELEMENT_TYPE_ARRAY is valid, but multidimensional arrays are not decoded here.
|
||||
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({
|
||||
fieldSignature: Buffer.from([6, 0x14, 8, 1, 0, 0]),
|
||||
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: "unsupported element type 0x14",
|
||||
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)}`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export const inspectManagedMembersBytes = (
|
||||
"Metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
|
||||
"CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.",
|
||||
"Exported types are reported from ExportedType rows as declared; forwarded types are not resolved in their implementing assemblies.",
|
||||
"Signatures are decoded for common ECMA-335 primitive, class, valuetype, pointer, byref, array, and generic variable forms; unsupported forms retain raw signature hashes.",
|
||||
"Member signatures decode ECMA-335 primitive, class, valuetype, generic instantiation, generic variable, pointer, byref, single-dimension and general array, function pointer, custom modifier, and typed reference forms; class and valuetype operands keep their encoded TypeDefOrRefOrSpec values, and unsupported forms such as vararg sentinels retain raw signature hashes.",
|
||||
],
|
||||
},
|
||||
managedDecodeBudget(layout),
|
||||
|
||||
@@ -346,50 +346,297 @@ const ELEMENT_TYPES = new Map<number, string>([
|
||||
[0x1c, "object"],
|
||||
]);
|
||||
|
||||
const readTypeSignature = (
|
||||
const readSignedCompressed = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
): { readonly value: number; readonly next: number } => {
|
||||
const encoded = readCompressed(bytes, offset);
|
||||
const width =
|
||||
encoded.next - offset === 1 ? 7 : encoded.next - offset === 2 ? 14 : 29;
|
||||
const magnitude = Math.floor(encoded.value / 2);
|
||||
return {
|
||||
value: encoded.value % 2 === 0 ? magnitude : magnitude - 2 ** (width - 1),
|
||||
next: encoded.next,
|
||||
};
|
||||
};
|
||||
|
||||
const malformedSignature = (detail: string, offset: number): never => {
|
||||
throw managedFailure("invalid-blob", "signature", detail, offset);
|
||||
};
|
||||
|
||||
// Pending work while decoding one Type. Signatures nest arbitrarily deep, so
|
||||
// decoding keeps an explicit stack instead of recursing.
|
||||
type TypeContinuation =
|
||||
| { readonly kind: "suffix"; readonly suffix: "*" | "&" | "[]" }
|
||||
| { readonly kind: "modifiers"; readonly modifiers: string[] }
|
||||
| { readonly kind: "array" }
|
||||
| {
|
||||
readonly kind: "list";
|
||||
remaining: number;
|
||||
readonly items: string[];
|
||||
// Function pointer items are a RetType then Params; generic
|
||||
// arguments are Types.
|
||||
readonly signatureItems: boolean;
|
||||
readonly render: (items: readonly string[]) => string;
|
||||
};
|
||||
|
||||
interface MethodSignatureHeader {
|
||||
readonly first: number;
|
||||
readonly genericParameterCount: number | null;
|
||||
readonly parameterCount: number;
|
||||
readonly next: number;
|
||||
}
|
||||
|
||||
const readMethodSignatureHeader = (
|
||||
blob: Buffer,
|
||||
offset: number,
|
||||
): MethodSignatureHeader => {
|
||||
const first = blob[offset];
|
||||
if (first === undefined)
|
||||
return malformedSignature("truncated method signature", offset);
|
||||
let next = offset + 1;
|
||||
let genericParameterCount: number | null = null;
|
||||
if ((first & 0x10) !== 0) {
|
||||
const generic = readCompressed(blob, next);
|
||||
genericParameterCount = generic.value;
|
||||
next = generic.next;
|
||||
}
|
||||
const parameterCount = readCompressed(blob, next);
|
||||
return {
|
||||
first,
|
||||
genericParameterCount,
|
||||
parameterCount: parameterCount.value,
|
||||
next: parameterCount.next,
|
||||
};
|
||||
};
|
||||
|
||||
const MAX_ARRAY_RANK = 32;
|
||||
|
||||
// FNPTR carries a method signature: managed DEFAULT/VARARG, or the unmanaged
|
||||
// C, STDCALL, THISCALL, FASTCALL, and UNMANAGED conventions that compilers
|
||||
// emit for unmanaged function pointers. FIELD, LOCAL_SIG, PROPERTY, and
|
||||
// GENERICINST headers are not method signatures.
|
||||
const FUNCTION_POINTER_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]);
|
||||
|
||||
const arrayShape = (
|
||||
blob: Buffer,
|
||||
offset: number,
|
||||
): { readonly value: string; readonly next: number } => {
|
||||
const suffixes: string[] = [];
|
||||
let cursor = offset;
|
||||
let kind = blob[cursor];
|
||||
while (kind === 0x0f || kind === 0x10 || kind === 0x1d) {
|
||||
suffixes.push(kind === 0x0f ? "*" : kind === 0x10 ? "&" : "[]");
|
||||
cursor += 1;
|
||||
kind = blob[cursor];
|
||||
}
|
||||
if (kind === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-blob",
|
||||
"signature",
|
||||
"truncated type signature",
|
||||
cursor,
|
||||
);
|
||||
const named = ELEMENT_TYPES.get(kind);
|
||||
let value: string;
|
||||
let next: number;
|
||||
if (named !== undefined) {
|
||||
value = named;
|
||||
next = cursor + 1;
|
||||
} else if (kind === 0x11 || kind === 0x12) {
|
||||
const token = readCompressed(blob, cursor + 1);
|
||||
value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
|
||||
next = token.next;
|
||||
} else if (kind === 0x1e || kind === 0x13) {
|
||||
const variable = readCompressed(blob, cursor + 1);
|
||||
value = `${kind === 0x1e ? "mvar" : "var"}:${String(variable.value)}`;
|
||||
next = variable.next;
|
||||
} else {
|
||||
const rank = readCompressed(blob, offset);
|
||||
if (rank.value === 0) return malformedSignature("array rank is zero", offset);
|
||||
// ECMA-335 leaves rank unbounded, but the CLR loads at most 32 dimensions;
|
||||
// larger ranks would expand a few bytes into an unbounded rendering.
|
||||
if (rank.value > MAX_ARRAY_RANK)
|
||||
throw managedFailure(
|
||||
"unsupported-signature",
|
||||
"signature",
|
||||
`unsupported element type 0x${kind.toString(16)}`,
|
||||
cursor,
|
||||
`array rank ${String(rank.value)} exceeds the ${String(MAX_ARRAY_RANK)} dimensions the CLR supports`,
|
||||
offset,
|
||||
);
|
||||
const sizeCount = readCompressed(blob, rank.next);
|
||||
if (sizeCount.value > rank.value)
|
||||
return malformedSignature("array has more sizes than dimensions", offset);
|
||||
const sizes: number[] = [];
|
||||
let next = sizeCount.next;
|
||||
for (let index = 0; index < sizeCount.value; index += 1) {
|
||||
const size = readCompressed(blob, next);
|
||||
sizes.push(size.value);
|
||||
next = size.next;
|
||||
}
|
||||
return { value: value + suffixes.reverse().join(""), next };
|
||||
const boundCount = readCompressed(blob, next);
|
||||
if (boundCount.value > rank.value)
|
||||
return malformedSignature(
|
||||
"array has more lower bounds than dimensions",
|
||||
offset,
|
||||
);
|
||||
const lowerBounds: number[] = [];
|
||||
next = boundCount.next;
|
||||
for (let index = 0; index < boundCount.value; index += 1) {
|
||||
const bound = readSignedCompressed(blob, next);
|
||||
lowerBounds.push(bound.value);
|
||||
next = bound.next;
|
||||
}
|
||||
const dimensions = Array.from({ length: rank.value }, (_, index) => {
|
||||
const size = sizes[index];
|
||||
const lower = lowerBounds[index];
|
||||
if (lower !== undefined)
|
||||
return size === undefined
|
||||
? `${String(lower)}...`
|
||||
: `${String(lower)}...${String(lower + size - 1)}`;
|
||||
if (size !== undefined) return String(size);
|
||||
// ILAsm spelling: a bare rank-1 `[]` is reserved for SZARRAY.
|
||||
return rank.value === 1 ? "..." : "";
|
||||
});
|
||||
return { value: `[${dimensions.join(",")}]`, next };
|
||||
};
|
||||
|
||||
/** Grammar position of the element being decoded (ECMA-335 II.23.2). */
|
||||
type SignaturePosition = "field" | "return" | "parameter" | "type" | "pointee";
|
||||
|
||||
// Custom modifiers do not change the position they precede.
|
||||
const currentPosition = (
|
||||
stack: readonly TypeContinuation[],
|
||||
base: SignaturePosition,
|
||||
): SignaturePosition => {
|
||||
const frame = stack.findLast(({ kind }) => kind !== "modifiers");
|
||||
if (frame === undefined) return base;
|
||||
if (frame.kind === "suffix") return frame.suffix === "*" ? "pointee" : "type";
|
||||
if (frame.kind === "list" && frame.signatureItems)
|
||||
return frame.items.length === 0 ? "return" : "parameter";
|
||||
return "type";
|
||||
};
|
||||
|
||||
// VOID is a RetType or a pointee; TYPEDBYREF is a RetType or Param; BYREF
|
||||
// prefixes a RetType, Param, or (since ref fields) a field type.
|
||||
const POSITION_ELEMENTS: ReadonlyMap<number, readonly SignaturePosition[]> =
|
||||
new Map([
|
||||
[0x01, ["return", "pointee"]],
|
||||
[0x16, ["return", "parameter"]],
|
||||
[0x10, ["return", "parameter", "field"]],
|
||||
]);
|
||||
|
||||
const readTypeSignature = (
|
||||
blob: Buffer,
|
||||
offset: number,
|
||||
base: SignaturePosition,
|
||||
): { readonly value: string; readonly next: number } => {
|
||||
const stack: TypeContinuation[] = [];
|
||||
let cursor = offset;
|
||||
for (;;) {
|
||||
let value: string | undefined;
|
||||
const kind = blob[cursor];
|
||||
if (kind === undefined)
|
||||
return malformedSignature("truncated type signature", cursor);
|
||||
const positions = POSITION_ELEMENTS.get(kind);
|
||||
if (
|
||||
positions !== undefined &&
|
||||
!positions.includes(currentPosition(stack, base))
|
||||
)
|
||||
return malformedSignature(
|
||||
`element type 0x${kind.toString(16)} is not valid in a ${currentPosition(stack, base)} position`,
|
||||
cursor,
|
||||
);
|
||||
const named = ELEMENT_TYPES.get(kind);
|
||||
if (named !== undefined) {
|
||||
value = named;
|
||||
cursor += 1;
|
||||
} else if (kind === 0x0f || kind === 0x10 || kind === 0x1d) {
|
||||
const suffix = kind === 0x0f ? "*" : kind === 0x10 ? "&" : "[]";
|
||||
stack.push({ kind: "suffix", suffix });
|
||||
cursor += 1;
|
||||
} else if (kind === 0x1f || kind === 0x20) {
|
||||
const token = readCompressed(blob, cursor + 1);
|
||||
const modifier = `${kind === 0x1f ? "modreq" : "modopt"}:${String(token.value)}`;
|
||||
// A modifier frame on top was pushed by the directly preceding byte;
|
||||
// keep an adjacent run together and in source order.
|
||||
const top = stack.at(-1);
|
||||
if (top?.kind === "modifiers") top.modifiers.push(modifier);
|
||||
else stack.push({ kind: "modifiers", modifiers: [modifier] });
|
||||
cursor = token.next;
|
||||
} else if (kind === 0x11 || kind === 0x12) {
|
||||
const token = readCompressed(blob, cursor + 1);
|
||||
value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
|
||||
cursor = token.next;
|
||||
} else if (kind === 0x1e || kind === 0x13) {
|
||||
const variable = readCompressed(blob, cursor + 1);
|
||||
value = `${kind === 0x1e ? "mvar" : "var"}:${String(variable.value)}`;
|
||||
cursor = variable.next;
|
||||
} else if (kind === 0x16) {
|
||||
value = "typedbyref";
|
||||
cursor += 1;
|
||||
} else if (kind === 0x14) {
|
||||
stack.push({ kind: "array" });
|
||||
cursor += 1;
|
||||
} else if (kind === 0x15) {
|
||||
const generic = blob[cursor + 1];
|
||||
if (generic !== 0x11 && generic !== 0x12)
|
||||
return malformedSignature(
|
||||
"generic instantiation does not name a class or value type",
|
||||
cursor + 1,
|
||||
);
|
||||
const token = readCompressed(blob, cursor + 2);
|
||||
const argumentCount = readCompressed(blob, token.next);
|
||||
if (argumentCount.value === 0)
|
||||
return malformedSignature(
|
||||
"generic instantiation has no type arguments",
|
||||
token.next,
|
||||
);
|
||||
const head = `${generic === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
|
||||
stack.push({
|
||||
kind: "list",
|
||||
remaining: argumentCount.value,
|
||||
items: [],
|
||||
signatureItems: false,
|
||||
render: (items) => `${head}<${items.join(",")}>`,
|
||||
});
|
||||
cursor = argumentCount.next;
|
||||
} else if (kind === 0x1b) {
|
||||
const header = readMethodSignatureHeader(blob, cursor + 1);
|
||||
if (
|
||||
!FUNCTION_POINTER_CONVENTIONS.has(header.first & 0x0f) ||
|
||||
(header.first & 0x80) !== 0 ||
|
||||
((header.first & 0x40) !== 0 && (header.first & 0x20) === 0)
|
||||
)
|
||||
return malformedSignature(
|
||||
`function pointer calling convention 0x${header.first.toString(16)} does not head a method signature`,
|
||||
cursor + 1,
|
||||
);
|
||||
const convention = callingConvention(header.first);
|
||||
const head =
|
||||
header.genericParameterCount === null
|
||||
? convention
|
||||
: `${convention};${String(header.genericParameterCount)}`;
|
||||
stack.push({
|
||||
kind: "list",
|
||||
remaining: header.parameterCount + 1,
|
||||
items: [],
|
||||
signatureItems: true,
|
||||
render: ([returnType, ...parameters]) =>
|
||||
`fnptr[${head}](${parameters.join(",")})->${returnType ?? ""}`,
|
||||
});
|
||||
cursor = header.next;
|
||||
} else {
|
||||
throw managedFailure(
|
||||
"unsupported-signature",
|
||||
"signature",
|
||||
`unsupported element type 0x${kind.toString(16)}`,
|
||||
cursor,
|
||||
);
|
||||
}
|
||||
while (value !== undefined) {
|
||||
const top = stack.pop();
|
||||
if (top === undefined) return { value, next: cursor };
|
||||
if (top.kind === "suffix") value += top.suffix;
|
||||
else if (top.kind === "modifiers")
|
||||
value = `${value} ${top.modifiers.join(" ")}`;
|
||||
else if (top.kind === "array") {
|
||||
const shape = arrayShape(blob, cursor);
|
||||
value += shape.value;
|
||||
cursor = shape.next;
|
||||
} else {
|
||||
top.items.push(value);
|
||||
top.remaining -= 1;
|
||||
if (top.remaining === 0) value = top.render(top.items);
|
||||
else {
|
||||
value = undefined;
|
||||
stack.push(top);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const CALLING_CONVENTION_NAMES = new Map([
|
||||
[0, "default"],
|
||||
[1, "c"],
|
||||
[2, "stdcall"],
|
||||
[3, "thiscall"],
|
||||
[4, "fastcall"],
|
||||
[5, "vararg"],
|
||||
[6, "field"],
|
||||
[9, "unmanaged"],
|
||||
]);
|
||||
|
||||
const callingConvention = (value: number): string => {
|
||||
const base = value & 0x0f;
|
||||
const flags = [
|
||||
@@ -397,14 +644,7 @@ const callingConvention = (value: number): string => {
|
||||
(value & 0x40) === 0 ? null : "explicit-this",
|
||||
(value & 0x10) === 0 ? null : "generic",
|
||||
].filter((flag) => flag !== null);
|
||||
const name =
|
||||
base === 0
|
||||
? "default"
|
||||
: base === 5
|
||||
? "vararg"
|
||||
: base === 6
|
||||
? "field"
|
||||
: `unknown:${String(base)}`;
|
||||
const name = CALLING_CONVENTION_NAMES.get(base) ?? `unknown:${String(base)}`;
|
||||
return flags.length === 0 ? name : `${name} ${flags.join(" ")}`;
|
||||
};
|
||||
|
||||
@@ -421,7 +661,7 @@ export const signature = (
|
||||
throw managedFailure("invalid-blob", "signature", "empty signature", 0);
|
||||
const first = blob[0] ?? 0;
|
||||
if ((first & 0x0f) === 6) {
|
||||
const fieldType = readTypeSignature(blob, 1);
|
||||
const fieldType = readTypeSignature(blob, 1, "field");
|
||||
return {
|
||||
...raw,
|
||||
kind: "field",
|
||||
@@ -436,20 +676,12 @@ export const signature = (
|
||||
fieldType.next === blob.length ? null : "Trailing signature data",
|
||||
};
|
||||
}
|
||||
let offset = 1;
|
||||
let genericParameterCount: number | null = null;
|
||||
if ((first & 0x10) !== 0) {
|
||||
const generic = readCompressed(blob, offset);
|
||||
genericParameterCount = generic.value;
|
||||
offset = generic.next;
|
||||
}
|
||||
const parameterCount = readCompressed(blob, offset);
|
||||
offset = parameterCount.next;
|
||||
const returnType = readTypeSignature(blob, offset);
|
||||
offset = returnType.next;
|
||||
const header = readMethodSignatureHeader(blob, 0);
|
||||
const returnType = readTypeSignature(blob, header.next, "return");
|
||||
let offset = returnType.next;
|
||||
const parameters: string[] = [];
|
||||
for (let index = 0; index < parameterCount.value; index += 1) {
|
||||
const parameter = readTypeSignature(blob, offset);
|
||||
for (let index = 0; index < header.parameterCount; index += 1) {
|
||||
const parameter = readTypeSignature(blob, offset, "parameter");
|
||||
parameters.push(parameter.value);
|
||||
offset = parameter.next;
|
||||
}
|
||||
@@ -458,8 +690,8 @@ export const signature = (
|
||||
kind: "method",
|
||||
parse_status: offset === blob.length ? "decoded" : "partial",
|
||||
calling_convention: callingConvention(first),
|
||||
generic_parameter_count: genericParameterCount,
|
||||
parameter_count: parameterCount.value,
|
||||
generic_parameter_count: header.genericParameterCount,
|
||||
parameter_count: header.parameterCount,
|
||||
return_type: returnType.value,
|
||||
parameter_types: parameters,
|
||||
field_type: null,
|
||||
|
||||
@@ -311,7 +311,7 @@ describe("managed member comparison of undecoded signatures", () => {
|
||||
it("pairs undecoded signatures by exact declared type, name, and raw bytes", () => {
|
||||
const undecoded = {
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
fieldSignature: Buffer.from([0x06, 0x7f]),
|
||||
};
|
||||
const left = inspect(
|
||||
buildManagedPeFixture(undecoded),
|
||||
@@ -346,7 +346,7 @@ describe("managed member comparison of undecoded signatures", () => {
|
||||
const left = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
fieldSignature: Buffer.from([0x06, 0x7f]),
|
||||
}),
|
||||
"/tmp/undecoded-before.dll",
|
||||
);
|
||||
@@ -374,7 +374,7 @@ describe("managed member comparison of undecoded signatures", () => {
|
||||
const right = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
fieldSignature: Buffer.from([0x06, 0x7f]),
|
||||
}),
|
||||
"/tmp/undecoded-after.dll",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user