mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
1176 lines
37 KiB
TypeScript
1176 lines
37 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import {
|
|
appendManagedParseIssues,
|
|
managedFailure,
|
|
ManagedReaderFailure,
|
|
readManagedValue,
|
|
} from "./ManagedReaderFailure.js";
|
|
import type {
|
|
ManagedMemberInspection,
|
|
ManagedParseIssue,
|
|
} from "../domain/managed/managedArtifact.js";
|
|
import {
|
|
managedDecodeBudget,
|
|
type ManagedDecodeBudget,
|
|
} from "./ManagedDecodeBudget.js";
|
|
import {
|
|
readManagedTypeNesting,
|
|
type ManagedTypeNesting,
|
|
} from "./ManagedTypeNesting.js";
|
|
import type {
|
|
ManagedMetadataLayout,
|
|
MetadataTableLayout,
|
|
} from "./ManagedMetadataLayout.js";
|
|
import {
|
|
readManagedCompressedUnsigned,
|
|
decodeMetadataCodedIndex,
|
|
metadataCodedIndexCoordinate,
|
|
type MetadataCodedIndexCoordinate,
|
|
type MetadataTokenFact,
|
|
metadataToken,
|
|
readMetadataString,
|
|
sha256Bytes,
|
|
} from "./ManagedMetadataHeaps.js";
|
|
import { memberListTable } from "./ManagedMemberPointers.js";
|
|
import type {
|
|
TypeMemberListEntry,
|
|
TypeMemberListFacts,
|
|
} from "./ManagedTypeMemberLists.js";
|
|
import type { TypeDefinitionRowFacts } from "./ManagedTypeDefinitionRows.js";
|
|
|
|
export type ManagedType = ManagedMemberInspection["types"][number];
|
|
/** A renderable TypeDef projection paired with its original source row. */
|
|
export interface DecodedTypeDefinition {
|
|
readonly row: number;
|
|
readonly type: ManagedType;
|
|
}
|
|
|
|
export type ManagedExportedType = NonNullable<
|
|
ManagedMemberInspection["exported_types"]
|
|
>[number];
|
|
export type ManagedField = ManagedMemberInspection["fields"][number];
|
|
export type ManagedMethod = ManagedMemberInspection["methods"][number];
|
|
export type ManagedMemberRef = ManagedMemberInspection["member_refs"][number];
|
|
export type ManagedCallEdge = ManagedMemberInspection["call_edges"][number];
|
|
export type ManagedFieldAccess =
|
|
ManagedMemberInspection["field_accesses"][number];
|
|
export type ManagedUserString = NonNullable<
|
|
ManagedMemberInspection["user_strings"]
|
|
>["literals"][number];
|
|
export type ManagedSignature = ManagedMethod["signature"];
|
|
export type ManagedMethodBody = ManagedMethod["body"];
|
|
export type ManagedInstructionAnchor = ManagedMethodBody["anchors"][number];
|
|
export type ManagedExceptionRegion =
|
|
ManagedMethodBody["exception_regions"][number];
|
|
|
|
export interface TypeRange {
|
|
readonly token: string;
|
|
readonly fullName: string | null;
|
|
readonly fieldStart: number | null;
|
|
readonly fieldEnd: number | null;
|
|
readonly methodStart: number | null;
|
|
readonly methodEnd: number | null;
|
|
}
|
|
|
|
/** One TypeDef row's names and coordinates, including unreadable declarations. */
|
|
interface TypeDefinition extends TypeRange {
|
|
readonly source: TypeDefinitionRowFacts;
|
|
readonly row: number;
|
|
readonly name: string | undefined;
|
|
readonly namespace: string | undefined;
|
|
}
|
|
|
|
/**
|
|
* Name and declaring type of a field, method, member reference, or method
|
|
* specification, keyed by its token so edges resolve any target uniformly.
|
|
*/
|
|
export interface MemberCore {
|
|
readonly token: string;
|
|
readonly name: string;
|
|
readonly declaringType: string | null;
|
|
/** Fields and methods by table; member references by decoded signature. */
|
|
readonly kind: ManagedSignature["kind"];
|
|
/**
|
|
* Generic arity from the owner's decoded signature (see
|
|
* `signatureGenericArity`); absent when no owner decoded it, as for a
|
|
* MethodDef outside the type selection.
|
|
*/
|
|
readonly genericArity?: number | null;
|
|
/** Nominal declaring type, name and decoded signature; never runtime binding. */
|
|
readonly identitySha256?: string | null;
|
|
}
|
|
|
|
export const memberTargetIdentity = (
|
|
name: string,
|
|
declaringType: Pick<MetadataTokenFact, "table" | "row"> | null,
|
|
signature: ManagedSignature,
|
|
typeName: DeclarationTypeName,
|
|
): string | null => {
|
|
if (
|
|
declaringType === null ||
|
|
signature.resolved_sha256 === null ||
|
|
signature.parse_status !== "decoded"
|
|
)
|
|
return null;
|
|
const { table, row } = declaringType;
|
|
if (table !== 1 && table !== 2) return null;
|
|
const owner = typeName(table, row);
|
|
return owner === null
|
|
? null
|
|
: createHash("sha256")
|
|
.update("managed-member-nominal-v1\0")
|
|
.update(owner)
|
|
.update(Buffer.from(signature.resolved_sha256, "hex"))
|
|
.update(name)
|
|
.digest("hex");
|
|
};
|
|
|
|
export interface ParsedInstruction {
|
|
readonly offset: number;
|
|
readonly opcode: string;
|
|
readonly operandKind: ManagedInstructionAnchor["operand_kind"];
|
|
readonly operand: string | null;
|
|
readonly reference: MetadataTokenFact | null;
|
|
}
|
|
|
|
export type ParsedTokenInstruction = ParsedInstruction & {
|
|
readonly reference: MetadataTokenFact;
|
|
};
|
|
|
|
export const isParsedTokenInstruction = (
|
|
instruction: ParsedInstruction,
|
|
): instruction is ParsedTokenInstruction => instruction.reference !== null;
|
|
|
|
export interface DecodedMethod {
|
|
readonly method: ManagedMethod;
|
|
readonly tokenInstructions: readonly ParsedTokenInstruction[];
|
|
readonly catchTokens: ReadonlyMap<number, MetadataTokenFact>;
|
|
}
|
|
|
|
const rowRange = (
|
|
table: MetadataTableLayout | undefined,
|
|
start: number | null,
|
|
end: number | null,
|
|
): {
|
|
readonly first: number | null;
|
|
readonly last: number | null;
|
|
readonly count: number;
|
|
} => {
|
|
const total = table?.rowCount ?? 0;
|
|
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 =>
|
|
namespace.length === 0 ? name : `${namespace}.${name}`;
|
|
|
|
const readTypeDefinition = (
|
|
bytes: Buffer,
|
|
layout: ManagedMetadataLayout,
|
|
row: number,
|
|
issues: ManagedParseIssue[],
|
|
current: TypeMemberListEntry,
|
|
): TypeDefinition => {
|
|
const { source } = current;
|
|
const { nameIndex, namespaceIndex } = source;
|
|
// Keep both independent name failures, but decode each heap coordinate once.
|
|
const namespace = readManagedValue(
|
|
() =>
|
|
readMetadataString(bytes, layout, namespaceIndex, layout.strings.size),
|
|
issues,
|
|
);
|
|
const name = readManagedValue(
|
|
() => readMetadataString(bytes, layout, nameIndex, layout.strings.size),
|
|
issues,
|
|
);
|
|
return {
|
|
source,
|
|
row,
|
|
name,
|
|
namespace,
|
|
token: metadataToken(2, row),
|
|
fullName:
|
|
namespace === undefined || name === undefined
|
|
? null
|
|
: fullName(namespace, name),
|
|
fieldStart: current.field.start.value,
|
|
fieldEnd: current.field.end,
|
|
methodStart: current.method.start.value,
|
|
methodEnd: current.method.end,
|
|
};
|
|
};
|
|
|
|
export const readTypeDefinitions = (
|
|
bytes: Buffer,
|
|
layout: ManagedMetadataLayout,
|
|
issues: ManagedParseIssue[],
|
|
memberLists: TypeMemberListFacts,
|
|
): readonly TypeDefinition[] => {
|
|
appendManagedParseIssues(issues, memberLists.issues);
|
|
const definitions: TypeDefinition[] = [];
|
|
for (const [index, entry] of memberLists.entries.entries())
|
|
definitions.push(
|
|
readTypeDefinition(bytes, layout, index + 1, issues, entry),
|
|
);
|
|
return definitions;
|
|
};
|
|
|
|
/**
|
|
* Enclosing TypeDef row of each nested TypeDef row, from the NestedClass
|
|
* table. Nesting is complete only when every row was read and valid.
|
|
*/
|
|
export const parseTypes = (
|
|
bytes: Buffer,
|
|
layout: ManagedMetadataLayout,
|
|
definitions: readonly TypeDefinition[],
|
|
): {
|
|
readonly types: readonly DecodedTypeDefinition[];
|
|
readonly issues: readonly ManagedParseIssue[];
|
|
readonly nesting: ManagedTypeNesting;
|
|
} => {
|
|
const fields = memberListTable(layout, 4);
|
|
const methods = memberListTable(layout, 6);
|
|
const items: DecodedTypeDefinition[] = [];
|
|
const issues: ManagedParseIssue[] = [];
|
|
const nesting = readManagedTypeNesting(bytes, layout, issues);
|
|
for (const definition of definitions) {
|
|
const { row, name, namespace, token } = definition;
|
|
if (
|
|
name === undefined ||
|
|
namespace === undefined ||
|
|
definition.fullName === null
|
|
)
|
|
continue;
|
|
const { flags, extendsRaw, extendsOffset, rowOffset } = definition.source;
|
|
const fieldRange = rowRange(
|
|
fields,
|
|
definition.fieldStart,
|
|
definition.fieldEnd,
|
|
);
|
|
const methodRange = rowRange(
|
|
methods,
|
|
definition.methodStart,
|
|
definition.methodEnd,
|
|
);
|
|
const extendsIndex = decodeMetadataCodedIndex(
|
|
extendsRaw,
|
|
2,
|
|
[2, 1, 27],
|
|
layout.rowCounts,
|
|
);
|
|
if (extendsIndex.kind === "invalid")
|
|
issues.push({
|
|
code: "invalid-row",
|
|
scope: `metadata.TypeDef:${token}`,
|
|
offset: extendsOffset,
|
|
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsIndex.reason}`,
|
|
});
|
|
const enclosingRow = nesting.enclosing.get(row);
|
|
managedDecodeBudget(layout).reserve(128, rowOffset);
|
|
items.push({
|
|
row,
|
|
type: {
|
|
token,
|
|
row_offset: rowOffset,
|
|
namespace,
|
|
name,
|
|
full_name: definition.fullName,
|
|
flags,
|
|
extends_token:
|
|
extendsIndex.kind === "valid" ? extendsIndex.token : null,
|
|
enclosing_type_token:
|
|
enclosingRow === undefined ? null : metadataToken(2, enclosingRow),
|
|
name_sha256: null,
|
|
field_list: {
|
|
first_row: fieldRange.first,
|
|
last_row: fieldRange.last,
|
|
count: fieldRange.count,
|
|
},
|
|
method_list: {
|
|
first_row: methodRange.first,
|
|
last_row: methodRange.last,
|
|
count: methodRange.count,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
return { types: items, issues, nesting };
|
|
};
|
|
|
|
export const readCompressed = (
|
|
bytes: Buffer,
|
|
offset: number,
|
|
): { readonly value: number; readonly next: number } => {
|
|
const decoded = readManagedCompressedUnsigned(bytes, offset);
|
|
if (!decoded.ok)
|
|
throw managedFailure(
|
|
"invalid-blob",
|
|
"signature",
|
|
decoded.error.kind === "truncated"
|
|
? "truncated compressed integer"
|
|
: "reserved compressed integer",
|
|
offset,
|
|
);
|
|
return { value: decoded.value.value, next: offset + decoded.value.width };
|
|
};
|
|
|
|
const ELEMENT_TYPES = new Map<number, string>([
|
|
[0x01, "void"],
|
|
[0x02, "bool"],
|
|
[0x03, "char"],
|
|
[0x04, "i1"],
|
|
[0x05, "u1"],
|
|
[0x06, "i2"],
|
|
[0x07, "u2"],
|
|
[0x08, "i4"],
|
|
[0x09, "u4"],
|
|
[0x0a, "i8"],
|
|
[0x0b, "u8"],
|
|
[0x0c, "r4"],
|
|
[0x0d, "r8"],
|
|
[0x0e, "string"],
|
|
[0x18, "native-int"],
|
|
[0x19, "native-uint"],
|
|
[0x1c, "object"],
|
|
]);
|
|
|
|
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;
|
|
vararg?: boolean;
|
|
sentinelSeen?: boolean;
|
|
readonly render: (items: readonly string[]) => string;
|
|
};
|
|
|
|
interface MethodSignatureHeader {
|
|
readonly first: number;
|
|
readonly genericParameterCount: number | null;
|
|
readonly parameterCount: number;
|
|
readonly next: number;
|
|
}
|
|
|
|
// MethodDefSig and MethodRefSig headers (ECMA-335 II.23.2.1-2) without
|
|
// HASTHIS and EXPLICITTHIS: DEFAULT, VARARG, or GENERIC, which implies DEFAULT.
|
|
const MEMBER_METHOD_HEADERS = new Set([0x00, 0x05, 0x10]);
|
|
|
|
// FNPTR also carries the unmanaged C, STDCALL, THISCALL, FASTCALL, and
|
|
// UNMANAGED conventions that compilers emit for unmanaged function pointers.
|
|
const FUNCTION_POINTER_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]);
|
|
|
|
/**
|
|
* Read a method signature's calling convention and counts. A header that is
|
|
* not a method signature in its context (a MethodDef or MemberRef row, or an
|
|
* FNPTR type), sets the reserved bit, or sets EXPLICITTHIS without HASTHIS is
|
|
* malformed, so no caller consumes its counts. FIELD, LOCAL_SIG, PROPERTY,
|
|
* and GENERICINST headers are never method signatures.
|
|
*/
|
|
const readMethodSignatureHeader = (
|
|
blob: Buffer,
|
|
offset: number,
|
|
context: "member" | "function-pointer",
|
|
): MethodSignatureHeader => {
|
|
const first = blob[offset];
|
|
if (first === undefined)
|
|
return malformedSignature("truncated method signature", offset);
|
|
const allowed =
|
|
context === "member"
|
|
? MEMBER_METHOD_HEADERS.has(first & 0x9f)
|
|
: (first & 0x80) === 0 && FUNCTION_POINTER_CONVENTIONS.has(first & 0x0f);
|
|
if (!allowed || ((first & 0x40) !== 0 && (first & 0x20) === 0))
|
|
return malformedSignature(
|
|
`calling convention 0x${first.toString(16)} does not head a ${context} 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;
|
|
|
|
const arrayShape = (
|
|
blob: Buffer,
|
|
offset: number,
|
|
): { readonly value: string; readonly next: number } => {
|
|
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",
|
|
`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;
|
|
}
|
|
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"
|
|
| "local"
|
|
| "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, Param, or local;
|
|
// BYREF prefixes a RetType, Param, local, or (since ref fields) a field type.
|
|
const POSITION_ELEMENTS: ReadonlyMap<number, readonly SignaturePosition[]> =
|
|
new Map([
|
|
[0x01, ["return", "pointee"]],
|
|
[0x16, ["return", "parameter", "local"]],
|
|
[0x10, ["return", "parameter", "local", "field"]],
|
|
]);
|
|
|
|
/** Decode a type operand once; spec callers also validate its metadata coordinate. */
|
|
const readTypeOperand = (
|
|
blob: Buffer,
|
|
offset: number,
|
|
layout?: ManagedMetadataLayout,
|
|
tables: readonly number[] = [2, 1, 27],
|
|
): Omit<TypeOperand, "offset"> => {
|
|
const token = readCompressed(blob, offset);
|
|
if (layout !== undefined) {
|
|
const decoded = decodeMetadataCodedIndex(
|
|
token.value,
|
|
2,
|
|
tables,
|
|
layout.rowCounts,
|
|
);
|
|
if (decoded.kind !== "valid")
|
|
throw managedFailure(
|
|
"invalid-row",
|
|
"signature",
|
|
`TypeDefOrRefOrSpec operand 0x${token.value.toString(16)} is invalid: ${decoded.kind === "null" ? "type operand is null" : decoded.reason}`,
|
|
offset,
|
|
);
|
|
return { ...token, coordinate: { table: decoded.table, row: decoded.row } };
|
|
}
|
|
const { table, row } = metadataCodedIndexCoordinate(token.value, 2, tables);
|
|
return { ...token, coordinate: { table, row } };
|
|
};
|
|
|
|
/** A TypeDefOrRefOrSpec coded index embedded in a signature blob. */
|
|
interface TypeOperand {
|
|
readonly offset: number;
|
|
readonly next: number;
|
|
readonly value: number;
|
|
readonly coordinate: MetadataCodedIndexCoordinate;
|
|
}
|
|
|
|
interface NominalGenericType {
|
|
readonly table: 1 | 2;
|
|
readonly row: number;
|
|
}
|
|
|
|
const readTypeSignature = (
|
|
blob: Buffer,
|
|
offset: number,
|
|
base: SignaturePosition,
|
|
{
|
|
layout,
|
|
operands,
|
|
}: {
|
|
/** Validate each type operand against these tables, as specs require. */
|
|
readonly layout?: ManagedMetadataLayout;
|
|
/** Collect each type operand, to resolve it by name. */
|
|
readonly operands?: TypeOperand[];
|
|
} = {},
|
|
): {
|
|
readonly value: string;
|
|
readonly next: number;
|
|
readonly nominalGenericType: NominalGenericType | null;
|
|
} => {
|
|
const stack: TypeContinuation[] = [];
|
|
let cursor = offset;
|
|
let nominalGenericType: NominalGenericType | null = null;
|
|
let pinned = false;
|
|
for (;;) {
|
|
let value: string | undefined;
|
|
const kind = blob[cursor];
|
|
if (kind === undefined)
|
|
return malformedSignature("truncated type signature", cursor);
|
|
if (kind === 0x41) {
|
|
const signature = stack.at(-1);
|
|
if (signature?.kind !== "list" || !signature.signatureItems)
|
|
throw managedFailure(
|
|
"unsupported-signature",
|
|
"signature",
|
|
"unsupported element type 0x41",
|
|
cursor,
|
|
);
|
|
if (!signature.vararg)
|
|
throw managedFailure(
|
|
"unsupported-signature",
|
|
"signature",
|
|
"unsupported element type 0x41",
|
|
cursor,
|
|
);
|
|
if (signature.sentinelSeen || signature.remaining === 0)
|
|
return malformedSignature(
|
|
"VARARG function pointer has an invalid sentinel position",
|
|
cursor,
|
|
);
|
|
signature.sentinelSeen = true;
|
|
cursor += 1;
|
|
if (blob[cursor] === undefined || blob[cursor] === 0x41)
|
|
return malformedSignature(
|
|
"VARARG function pointer has adjacent sentinels",
|
|
cursor,
|
|
);
|
|
continue;
|
|
}
|
|
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,
|
|
);
|
|
if (kind === 0x45) {
|
|
if (base !== "local")
|
|
throw managedFailure(
|
|
"unsupported-signature",
|
|
"signature",
|
|
"unsupported element type 0x45",
|
|
cursor,
|
|
);
|
|
if (
|
|
pinned ||
|
|
stack.some(({ kind: frameKind }) => frameKind !== "modifiers")
|
|
)
|
|
return malformedSignature(
|
|
"PINNED is not valid in this type position",
|
|
cursor,
|
|
);
|
|
pinned = true;
|
|
cursor += 1;
|
|
continue;
|
|
}
|
|
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) {
|
|
if (pinned && currentPosition(stack, base) === "local")
|
|
return malformedSignature(
|
|
"custom modifier follows the LocalVarSig PINNED constraint",
|
|
cursor,
|
|
);
|
|
const token = readTypeOperand(blob, cursor + 1, layout);
|
|
operands?.push({ offset: cursor + 1, ...token });
|
|
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 = readTypeOperand(blob, cursor + 1, layout);
|
|
operands?.push({ offset: cursor + 1, ...token });
|
|
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) {
|
|
if (pinned && currentPosition(stack, base) === "local")
|
|
return malformedSignature(
|
|
"TYPEDBYREF cannot have a LocalVarSig PINNED constraint",
|
|
cursor,
|
|
);
|
|
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 = readTypeOperand(blob, cursor + 2, layout, [2, 1]);
|
|
if (
|
|
cursor === offset &&
|
|
(token.coordinate.table === 1 || token.coordinate.table === 2)
|
|
)
|
|
nominalGenericType = {
|
|
table: token.coordinate.table,
|
|
row: token.coordinate.row,
|
|
};
|
|
operands?.push({ offset: cursor + 2, ...token });
|
|
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,
|
|
"function-pointer",
|
|
);
|
|
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,
|
|
vararg: (header.first & 0x0f) === 5,
|
|
sentinelSeen: false,
|
|
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, nominalGenericType };
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Validate a StandAloneSig LocalVarSig without projecting local type identities. */
|
|
export const validateLocalSignature = (
|
|
layout: ManagedMetadataLayout,
|
|
blob: Buffer,
|
|
{
|
|
scope,
|
|
blobOffset,
|
|
}: { readonly scope: string; readonly blobOffset: number },
|
|
budget: ManagedDecodeBudget,
|
|
): ManagedParseIssue | null => {
|
|
budget.admitTransient(blob.length * 256, blobOffset);
|
|
try {
|
|
if (blob[0] !== 0x07)
|
|
malformedSignature("StandAloneSig row does not contain a LocalVarSig", 0);
|
|
const count = readCompressed(blob, 1);
|
|
if (count.value === 0 || count.value > 0xfffe)
|
|
malformedSignature(
|
|
`LocalVarSig local count ${String(count.value)} is outside the ECMA-335 range`,
|
|
1,
|
|
);
|
|
let cursor = count.next;
|
|
for (let index = 0; index < count.value; index += 1) {
|
|
const local = readTypeSignature(blob, cursor, "local", { layout });
|
|
cursor = local.next;
|
|
}
|
|
if (cursor !== blob.length)
|
|
malformedSignature("Trailing LocalVarSig data", cursor);
|
|
return null;
|
|
} catch (cause: unknown) {
|
|
if (
|
|
!(cause instanceof ManagedReaderFailure) ||
|
|
(cause.issue.code !== "invalid-blob" &&
|
|
cause.issue.code !== "invalid-row" &&
|
|
cause.issue.code !== "unsupported-signature")
|
|
)
|
|
throw cause;
|
|
return {
|
|
...cause.issue,
|
|
scope,
|
|
offset:
|
|
cause.issue.offset === null
|
|
? blobOffset
|
|
: blobOffset + cause.issue.offset,
|
|
};
|
|
}
|
|
};
|
|
|
|
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 = [
|
|
(value & 0x20) === 0 ? null : "has-this",
|
|
(value & 0x40) === 0 ? null : "explicit-this",
|
|
(value & 0x10) === 0 ? null : "generic",
|
|
].filter((flag) => flag !== null);
|
|
const name = CALLING_CONVENTION_NAMES.get(base) ?? `unknown:${String(base)}`;
|
|
return flags.length === 0 ? name : `${name} ${flags.join(" ")}`;
|
|
};
|
|
|
|
/**
|
|
* Decode a whole TypeSpec blob, or a MethodSpec instantiation (GENERICINST,
|
|
* argument count, Types), with the member-signature grammar and return the
|
|
* rendered types. A malformed or unsupported blob throws invalid-blob or
|
|
* unsupported-signature under the owning row, so coverage records it.
|
|
*/
|
|
export const specSignatureFacts = (
|
|
layout: ManagedMetadataLayout,
|
|
blob: Buffer,
|
|
owner: {
|
|
readonly form: "type" | "instantiation";
|
|
readonly scope: string;
|
|
readonly offset: number;
|
|
},
|
|
typeName?: SignatureTypeName,
|
|
): {
|
|
readonly types: readonly string[];
|
|
readonly resolvedSha256: string | null;
|
|
readonly nominalGenericType: NominalGenericType | null;
|
|
} => {
|
|
const budget = managedDecodeBudget(layout);
|
|
// Rendering is transient, so charge it as member signatures are charged.
|
|
budget.admitTransient(blob.length * 256, owner.offset);
|
|
try {
|
|
let cursor = 0;
|
|
let count = 1;
|
|
if (owner.form === "instantiation") {
|
|
if (blob[0] !== 0x0a)
|
|
malformedSignature("MethodSpec instantiation is not GENERICINST", 0);
|
|
const header = readCompressed(blob, 1);
|
|
if (header.value === 0)
|
|
malformedSignature("MethodSpec instantiation has no type arguments", 1);
|
|
count = header.value;
|
|
cursor = header.next;
|
|
}
|
|
const types: string[] = [];
|
|
const operands: TypeOperand[] = [];
|
|
let nominalGenericType: NominalGenericType | null = null;
|
|
// Each Type consumes at least one byte, so a false count fails on truncation.
|
|
for (let index = 0; index < count; index += 1) {
|
|
const type = readTypeSignature(blob, cursor, "type", {
|
|
layout,
|
|
operands,
|
|
});
|
|
types.push(type.value);
|
|
if (owner.form === "type") nominalGenericType = type.nominalGenericType;
|
|
cursor = type.next;
|
|
}
|
|
if (cursor !== blob.length)
|
|
malformedSignature("Trailing signature data", cursor);
|
|
return {
|
|
types,
|
|
resolvedSha256:
|
|
typeName === undefined
|
|
? null
|
|
: resolvedSha256(blob, operands, typeName),
|
|
nominalGenericType,
|
|
};
|
|
} catch (cause: unknown) {
|
|
if (
|
|
!(cause instanceof ManagedReaderFailure) ||
|
|
(cause.issue.code !== "invalid-blob" &&
|
|
cause.issue.code !== "invalid-row" &&
|
|
cause.issue.code !== "unsupported-signature")
|
|
)
|
|
throw cause;
|
|
throw managedFailure(
|
|
cause.issue.code,
|
|
owner.scope,
|
|
cause.issue.detail,
|
|
owner.offset,
|
|
);
|
|
} finally {
|
|
budget.releaseTransient();
|
|
}
|
|
};
|
|
|
|
type DecodedSignature = Omit<
|
|
ManagedSignature,
|
|
"raw_length" | "raw_sha256" | "resolved_sha256"
|
|
>;
|
|
|
|
/**
|
|
* Decode a field or member method signature. A malformed or unsupported blob
|
|
* decodes as kind `unknown` with its reason.
|
|
*/
|
|
const decodeSignature = (
|
|
blob: Buffer,
|
|
layout?: ManagedMetadataLayout,
|
|
operands: TypeOperand[] = [],
|
|
issueSink?: (issue: ManagedParseIssue) => void,
|
|
scope = "signature",
|
|
): DecodedSignature => {
|
|
let kind: DecodedSignature["kind"] = "unknown";
|
|
try {
|
|
if (blob.length === 0)
|
|
throw managedFailure("invalid-blob", "signature", "empty signature", 0);
|
|
const first = blob[0] ?? 0;
|
|
// FieldSig (II.23.2.4) is exactly FIELD; a flagged FIELD header is not a
|
|
// method header either, so it is malformed below.
|
|
if (first === 0x06) {
|
|
kind = "field";
|
|
const fieldType = readTypeSignature(blob, 1, "field", {
|
|
operands,
|
|
...(layout === undefined ? {} : { layout }),
|
|
});
|
|
return {
|
|
kind: "field",
|
|
parse_status: fieldType.next === blob.length ? "decoded" : "partial",
|
|
calling_convention: callingConvention(first),
|
|
generic_parameter_count: null,
|
|
parameter_count: null,
|
|
return_type: null,
|
|
parameter_types: [],
|
|
field_type: fieldType.value,
|
|
issue:
|
|
fieldType.next === blob.length ? null : "Trailing signature data",
|
|
};
|
|
}
|
|
const header = readMethodSignatureHeader(blob, 0, "member");
|
|
kind = "method";
|
|
const returnType = readTypeSignature(blob, header.next, "return", {
|
|
operands,
|
|
...(layout === undefined ? {} : { layout }),
|
|
});
|
|
let offset = returnType.next;
|
|
const parameters: string[] = [];
|
|
let sentinelSeen = false;
|
|
for (let index = 0; index < header.parameterCount; index += 1) {
|
|
if (blob[offset] === 0x41 && (first & 0x0f) === 5) {
|
|
if (sentinelSeen)
|
|
malformedSignature(
|
|
"VARARG signature has an invalid sentinel position",
|
|
offset,
|
|
);
|
|
sentinelSeen = true;
|
|
offset += 1;
|
|
if (blob[offset] === undefined || blob[offset] === 0x41)
|
|
malformedSignature("VARARG signature has adjacent sentinels", offset);
|
|
}
|
|
const parameter = readTypeSignature(blob, offset, "parameter", {
|
|
operands,
|
|
...(layout === undefined ? {} : { layout }),
|
|
});
|
|
parameters.push(parameter.value);
|
|
offset = parameter.next;
|
|
}
|
|
return {
|
|
kind: "method",
|
|
parse_status: offset === blob.length ? "decoded" : "partial",
|
|
calling_convention: callingConvention(first),
|
|
generic_parameter_count: header.genericParameterCount,
|
|
parameter_count: header.parameterCount,
|
|
return_type: returnType.value,
|
|
parameter_types: parameters,
|
|
field_type: null,
|
|
issue: offset === blob.length ? null : "Trailing signature data",
|
|
};
|
|
} catch (cause: unknown) {
|
|
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
|
if (
|
|
cause.issue.code !== "invalid-blob" &&
|
|
cause.issue.code !== "invalid-row" &&
|
|
cause.issue.code !== "unsupported-signature"
|
|
)
|
|
throw cause;
|
|
if (cause.issue.code === "invalid-row")
|
|
issueSink?.({ ...cause.issue, scope });
|
|
return {
|
|
kind: cause.issue.code === "invalid-row" ? kind : "unknown",
|
|
parse_status:
|
|
cause.issue.code === "unsupported-signature"
|
|
? "unsupported"
|
|
: "malformed",
|
|
calling_convention: null,
|
|
generic_parameter_count: null,
|
|
parameter_count: null,
|
|
return_type: null,
|
|
parameter_types: [],
|
|
field_type: null,
|
|
issue: cause.issue.detail,
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generic parameter count of a method signature that decodes completely, 0
|
|
* when it is not generic, or null when the blob is not a decodable method
|
|
* signature. The rendered decode is transient.
|
|
*/
|
|
export const methodGenericArity = (
|
|
layout: ManagedMetadataLayout,
|
|
blob: Buffer,
|
|
): number | null => {
|
|
const budget = managedDecodeBudget(layout);
|
|
budget.admitTransient(
|
|
blob.length * 256,
|
|
layout.blob?.offset ?? layout.rootOffset,
|
|
);
|
|
try {
|
|
return signatureGenericArity(decodeSignature(blob));
|
|
} finally {
|
|
budget.releaseTransient();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generic parameter count of a completely decoded method signature, 0 when it
|
|
* is not generic, or null for any other signature.
|
|
*/
|
|
export const signatureGenericArity = (
|
|
decoded: DecodedSignature,
|
|
): number | null =>
|
|
decoded.kind === "method" && decoded.parse_status === "decoded"
|
|
? (decoded.generic_parameter_count ?? 0)
|
|
: null;
|
|
|
|
/**
|
|
* SHA-256 of the namespace-qualified name of a TypeDefOrRefOrSpec coded index,
|
|
* or null when it has no name.
|
|
*/
|
|
export type SignatureTypeName = (
|
|
operand: Pick<TypeOperand, "value" | "coordinate">,
|
|
) => Buffer | null;
|
|
|
|
export type DeclarationTypeName = (table: 1 | 2, row: number) => Buffer | null;
|
|
|
|
/**
|
|
* Each type operand is replaced by 0xff, which never starts a compressed
|
|
* coded index, then the 32-byte name digest; a blob without type operands
|
|
* keeps its raw digest. So no decodable signature's resolved preimage is
|
|
* another's, but an undecodable blob can hold one, and its raw digest then
|
|
* equals that resolved digest. Comparison keys the two kinds apart.
|
|
*/
|
|
const OPERAND_MARKER = Buffer.from([0xff]);
|
|
|
|
/**
|
|
* Digest the blob with each type operand replaced by its type's name digest,
|
|
* so the value does not depend on build-local row numbers.
|
|
*/
|
|
const resolvedSha256 = (
|
|
blob: Buffer,
|
|
operands: readonly TypeOperand[],
|
|
typeName: SignatureTypeName,
|
|
): string | null => {
|
|
const hash = createHash("sha256");
|
|
let copied = 0;
|
|
for (const operand of operands) {
|
|
const name = typeName(operand);
|
|
if (name === null) return null;
|
|
hash.update(blob.subarray(copied, operand.offset));
|
|
hash.update(OPERAND_MARKER);
|
|
hash.update(name);
|
|
copied = operand.next;
|
|
}
|
|
hash.update(blob.subarray(copied));
|
|
return hash.digest("hex");
|
|
};
|
|
|
|
const projectSignature = (
|
|
blob: Buffer,
|
|
rawSha256: string | undefined,
|
|
typeName: SignatureTypeName,
|
|
decoded: DecodedSignature,
|
|
operands: readonly TypeOperand[],
|
|
): ManagedSignature => {
|
|
return {
|
|
raw_length: blob.length,
|
|
raw_sha256: rawSha256 ?? sha256Bytes(blob),
|
|
resolved_sha256:
|
|
decoded.kind === "unknown" || decoded.parse_status === "malformed"
|
|
? null
|
|
: resolvedSha256(blob, operands, typeName),
|
|
...decoded,
|
|
};
|
|
};
|
|
|
|
/** Standalone signature decoding used by tests and signature identity utilities. */
|
|
export const signature = (
|
|
blob: Buffer,
|
|
rawSha256?: string,
|
|
typeName: SignatureTypeName = () => null,
|
|
): ManagedSignature => {
|
|
const operands: TypeOperand[] = [];
|
|
return projectSignature(
|
|
blob,
|
|
rawSha256,
|
|
typeName,
|
|
decodeSignature(blob, undefined, operands),
|
|
operands,
|
|
);
|
|
};
|
|
|
|
/** Decode a metadata-owned signature while validating referenced table rows. */
|
|
export const metadataSignature = (
|
|
blob: Buffer,
|
|
{
|
|
rawSha256,
|
|
typeName,
|
|
layout,
|
|
issues,
|
|
scope,
|
|
expectedKind,
|
|
}: {
|
|
readonly rawSha256: string;
|
|
readonly typeName: SignatureTypeName;
|
|
readonly layout: ManagedMetadataLayout;
|
|
readonly issues: ManagedParseIssue[];
|
|
readonly scope: string;
|
|
readonly expectedKind?: "field" | "method";
|
|
},
|
|
): ManagedSignature => {
|
|
const operands: TypeOperand[] = [];
|
|
let decoded = decodeSignature(
|
|
blob,
|
|
layout,
|
|
operands,
|
|
(issue) => issues.push(issue),
|
|
scope,
|
|
);
|
|
if (
|
|
expectedKind !== undefined &&
|
|
decoded.kind !== "unknown" &&
|
|
decoded.kind !== expectedKind
|
|
) {
|
|
const detail = `Expected ${expectedKind} signature; decoded ${decoded.kind}${decoded.issue === null ? "" : `: ${decoded.issue}`}`;
|
|
issues.push({ code: "invalid-blob", scope, offset: null, detail });
|
|
decoded = { ...decoded, parse_status: "malformed", issue: detail };
|
|
}
|
|
return projectSignature(blob, rawSha256, typeName, decoded, operands);
|
|
};
|