mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 03:15:54 +00:00
Keep same-named nested members paired by their source-owned enclosing identity, including selected inventories that omit the parent TypeDefs. Share the cached, bounded nominal type-name digest owner with signature resolution; preserve current unknown identity through matching, remapping and absence instead of repairing it with display names or historical compatibility. Verification: 457 related tests passed after preserving TypeDef visibility/parent applicability and coherent fixtures; the complete static/architecture gate and real pinned-SDK MCP nested/official .NET DLL workflows passed after a sigrok-experiment-only main refresh. Documentation checks passed after the compatibility fix. Full CoreLib inspection retains the pre-existing explicit representation limit. No target assembly execution; local verification used two CPUs and an 8 GiB RSS guard. Co-authored-by: 8rulerstar <190670431+8rulerstar@users.noreply.github.com> Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
1468 lines
43 KiB
TypeScript
1468 lines
43 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
|
|
|
interface ManagedPeFixtureOptions {
|
|
readonly additionalEmptyTypes?: number;
|
|
readonly customAttributeRowCount?: number;
|
|
readonly cliFlags?: number;
|
|
readonly corruptMetadataSignature?: boolean;
|
|
readonly customAttributeTypeRaw?: number;
|
|
readonly extendsRaw?: number;
|
|
readonly exportedTypes?: readonly {
|
|
readonly namespace: string;
|
|
readonly name: string;
|
|
readonly flags: number;
|
|
readonly implementationRaw: number;
|
|
}[];
|
|
readonly fieldName?: string;
|
|
readonly fieldSignature?: Buffer;
|
|
readonly ilBody?: Buffer;
|
|
readonly malformedAssemblyReferenceRows?: readonly number[];
|
|
readonly malformedCustomAttributeRows?: readonly number[];
|
|
readonly metadataExtraData?: number;
|
|
readonly metadataValidMaskExtra?: bigint;
|
|
readonly methodRowCount?: number;
|
|
readonly methodName?: string;
|
|
readonly methodFlags?: number;
|
|
readonly methodSignature?: Buffer;
|
|
readonly moduleRowCount?: number;
|
|
readonly assemblyRowCount?: number;
|
|
readonly memberRefParentRaw?: number;
|
|
readonly memberRefName?: string;
|
|
readonly memberRefSignature?: Buffer;
|
|
/** Raw MethodDefOrRef coded values, one MethodSpec row each. */
|
|
readonly methodSpecMethods?: readonly number[];
|
|
/** Instantiation blob shared by every MethodSpec row. */
|
|
readonly methodSpecInstantiation?: Buffer;
|
|
/** TypeSpec signature blobs, one TypeSpec row each. */
|
|
readonly typeSpecSignatures?: readonly Buffer[];
|
|
/** Raw ResolutionScope coded value of the TypeRef row; null by default. */
|
|
readonly typeRefScopeRaw?: number;
|
|
readonly mvid?: Buffer;
|
|
readonly pinvoke?: {
|
|
readonly importName?: string;
|
|
readonly memberForwardedRaw?: number;
|
|
readonly mappingFlags?: number;
|
|
readonly moduleName?: string;
|
|
};
|
|
readonly readyToRun?: boolean;
|
|
readonly references?: readonly string[];
|
|
readonly resourceImplementationRaw?: number;
|
|
readonly resourceData?: Buffer;
|
|
readonly targetFramework?: string | null;
|
|
readonly typeName?: string;
|
|
readonly typeNamespace?: string;
|
|
/** NestedClass rows as [nested, enclosing] TypeDef rows. */
|
|
readonly nestedTypes?: readonly (readonly [number, number])[];
|
|
/** TypeRef rows after the TargetFrameworkAttribute row, from row 2. */
|
|
readonly typeReferences?: readonly {
|
|
readonly namespace: string;
|
|
readonly name: string;
|
|
/** TypeRef row of the enclosing type, for a nested type. */
|
|
readonly enclosingRow?: number;
|
|
}[];
|
|
/** Raw #US stream bytes; omitted means the metadata has no #US stream. */
|
|
readonly userStringHeap?: Buffer;
|
|
readonly virtualSectionSize?: number;
|
|
}
|
|
|
|
const textEncoder = new TextEncoder();
|
|
|
|
const align4 = (value: number): number => (value + 3) & ~3;
|
|
|
|
const cString = (value: string): Buffer => Buffer.from(`${value}\0`, "utf8");
|
|
|
|
class StringHeap {
|
|
readonly #chunks: Buffer[] = [Buffer.from([0])];
|
|
#size = 1;
|
|
|
|
get size(): number {
|
|
return this.#size;
|
|
}
|
|
|
|
add(value: string): number {
|
|
if (value.length === 0) return 0;
|
|
const index = this.#size;
|
|
const chunk = cString(value);
|
|
this.#chunks.push(chunk);
|
|
this.#size += chunk.length;
|
|
return index;
|
|
}
|
|
|
|
toBuffer(): Buffer {
|
|
return Buffer.concat(this.#chunks);
|
|
}
|
|
}
|
|
|
|
class BlobHeap {
|
|
readonly #chunks: Buffer[] = [Buffer.from([0])];
|
|
#size = 1;
|
|
|
|
get size(): number {
|
|
return this.#size;
|
|
}
|
|
|
|
add(value: Buffer): number {
|
|
if (value.length === 0) return 0;
|
|
const index = this.#size;
|
|
const prefix =
|
|
value.length <= 0x7f
|
|
? Buffer.from([value.length])
|
|
: value.length <= 0x3fff
|
|
? Buffer.from([0x80 | (value.length >> 8), value.length & 0xff])
|
|
: Buffer.from([
|
|
0xc0 | (value.length >> 24),
|
|
(value.length >> 16) & 0xff,
|
|
(value.length >> 8) & 0xff,
|
|
value.length & 0xff,
|
|
]);
|
|
this.#chunks.push(prefix, value);
|
|
this.#size += value.length + prefix.length;
|
|
return index;
|
|
}
|
|
|
|
toBuffer(): Buffer {
|
|
return Buffer.concat(this.#chunks);
|
|
}
|
|
}
|
|
|
|
const DEFAULT_MVID = Buffer.from([
|
|
0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66, 0x88, 0x99, 0xaa, 0xbb, 0xcc,
|
|
0xdd, 0xee, 0xff,
|
|
]);
|
|
|
|
/** Describe fixture bytes as the managed PE target consumed by static inspectors. */
|
|
export const managedPeFixtureTarget = (
|
|
bytes: Buffer,
|
|
path = "/fixture.exe",
|
|
): BinaryTarget => ({
|
|
path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
kind: "executable",
|
|
format: "pe",
|
|
architecture: "x86",
|
|
availableArchitectures: ["x86"],
|
|
executableRole: "application",
|
|
managed: true,
|
|
});
|
|
|
|
const guidHeap = (mvid: Buffer = DEFAULT_MVID): Buffer => {
|
|
if (mvid.length !== 16)
|
|
throw new RangeError("Managed PE fixture MVID must be 16 bytes");
|
|
return Buffer.from(mvid);
|
|
};
|
|
|
|
const u16 = (value: number): Buffer => {
|
|
const bytes = Buffer.alloc(2);
|
|
bytes.writeUInt16LE(value, 0);
|
|
return bytes;
|
|
};
|
|
|
|
const u32 = (value: number): Buffer => {
|
|
const bytes = Buffer.alloc(4);
|
|
bytes.writeUInt32LE(value, 0);
|
|
return bytes;
|
|
};
|
|
|
|
type MetadataIndexSize = 2 | 4;
|
|
|
|
const indexSize = (rows: number, codedTagBits = 0): MetadataIndexSize =>
|
|
rows < 2 ** (16 - codedTagBits) ? 2 : 4;
|
|
|
|
const metadataIndex = (value: number, size: MetadataIndexSize): Buffer =>
|
|
size === 2 ? u16(value) : u32(value);
|
|
|
|
const codedIndexSize = (
|
|
rows: readonly number[],
|
|
tagBits: number,
|
|
): MetadataIndexSize => indexSize(Math.max(...rows), tagBits);
|
|
|
|
const moduleRow = (
|
|
name: number,
|
|
stringSize: MetadataIndexSize,
|
|
guidSize: MetadataIndexSize,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u16(0),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(1, guidSize),
|
|
metadataIndex(0, guidSize),
|
|
metadataIndex(0, guidSize),
|
|
]);
|
|
|
|
const typeRefRow = (
|
|
name: number,
|
|
namespace: number,
|
|
stringSize: MetadataIndexSize,
|
|
resolutionScope: Buffer,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
resolutionScope,
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(namespace, stringSize),
|
|
]);
|
|
|
|
const typeDefRow = ({
|
|
name,
|
|
namespace,
|
|
fieldList,
|
|
methodList,
|
|
stringSize,
|
|
typeDefOrRefSize,
|
|
fieldListSize,
|
|
methodListSize,
|
|
extendsRaw = (1 << 2) | 1,
|
|
flags = 0x0010_0001,
|
|
}: {
|
|
readonly name: number;
|
|
readonly namespace: number;
|
|
readonly fieldList: number;
|
|
readonly methodList: number;
|
|
readonly stringSize: MetadataIndexSize;
|
|
readonly typeDefOrRefSize: MetadataIndexSize;
|
|
readonly fieldListSize: MetadataIndexSize;
|
|
readonly methodListSize: MetadataIndexSize;
|
|
readonly extendsRaw?: number;
|
|
readonly flags?: number;
|
|
}): Buffer =>
|
|
Buffer.concat([
|
|
u32(flags),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(namespace, stringSize),
|
|
metadataIndex(extendsRaw, typeDefOrRefSize),
|
|
metadataIndex(fieldList, fieldListSize),
|
|
metadataIndex(methodList, methodListSize),
|
|
]);
|
|
|
|
const fieldRow = (
|
|
name: number,
|
|
signature: number,
|
|
stringSize: MetadataIndexSize,
|
|
blobSize: MetadataIndexSize,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u16(0x0001),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(signature, blobSize),
|
|
]);
|
|
|
|
interface MethodDefRowInput {
|
|
readonly name: number;
|
|
readonly signature: number;
|
|
readonly stringSize: MetadataIndexSize;
|
|
readonly blobSize: MetadataIndexSize;
|
|
readonly parameterSize: MetadataIndexSize;
|
|
readonly rva?: number;
|
|
readonly flags?: number;
|
|
readonly implFlags?: number;
|
|
readonly parameterList?: number;
|
|
}
|
|
|
|
const methodDefRow = ({
|
|
name,
|
|
signature,
|
|
stringSize,
|
|
blobSize,
|
|
parameterSize,
|
|
rva = 0,
|
|
flags = 0x0016,
|
|
implFlags = 0,
|
|
parameterList = 0,
|
|
}: MethodDefRowInput): Buffer =>
|
|
Buffer.concat([
|
|
u32(rva),
|
|
u16(implFlags),
|
|
u16(flags),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(signature, blobSize),
|
|
metadataIndex(parameterList, parameterSize),
|
|
]);
|
|
|
|
const memberRefRow = ({
|
|
name,
|
|
signature,
|
|
stringSize,
|
|
blobSize,
|
|
parentSize,
|
|
parentRaw = (1 << 3) | 1,
|
|
}: {
|
|
readonly name: number;
|
|
readonly signature: number;
|
|
readonly stringSize: MetadataIndexSize;
|
|
readonly blobSize: MetadataIndexSize;
|
|
readonly parentSize: MetadataIndexSize;
|
|
readonly parentRaw?: number;
|
|
}): Buffer =>
|
|
Buffer.concat([
|
|
metadataIndex(parentRaw, parentSize),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(signature, blobSize),
|
|
]);
|
|
|
|
const moduleRefRow = (name: number, stringSize: MetadataIndexSize): Buffer =>
|
|
Buffer.concat([metadataIndex(name, stringSize)]);
|
|
|
|
const implMapRow = (
|
|
importName: number,
|
|
importScope: number,
|
|
mappingFlags: number,
|
|
stringSize: MetadataIndexSize,
|
|
memberForwardedSize: MetadataIndexSize,
|
|
moduleRefSize: MetadataIndexSize,
|
|
memberForwardedRaw: number,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u16(mappingFlags),
|
|
metadataIndex(memberForwardedRaw, memberForwardedSize),
|
|
metadataIndex(importName, stringSize),
|
|
metadataIndex(importScope, moduleRefSize),
|
|
]);
|
|
|
|
const assemblyRow = (
|
|
name: number,
|
|
stringSize: MetadataIndexSize,
|
|
blobSize: MetadataIndexSize,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u32(0x0000_8004),
|
|
u16(1),
|
|
u16(2),
|
|
u16(3),
|
|
u16(4),
|
|
u32(0),
|
|
metadataIndex(0, blobSize),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(0, stringSize),
|
|
]);
|
|
|
|
const assemblyRefRow = (
|
|
name: number,
|
|
keyOrToken: number,
|
|
malformedName: boolean,
|
|
stringSize: MetadataIndexSize,
|
|
blobSize: MetadataIndexSize,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u16(8),
|
|
u16(0),
|
|
u16(0),
|
|
u16(0),
|
|
u32(0),
|
|
metadataIndex(keyOrToken, blobSize),
|
|
metadataIndex(
|
|
malformedName ? (stringSize === 2 ? 0xffff : 0xffff_ffff) : name,
|
|
stringSize,
|
|
),
|
|
metadataIndex(0, stringSize),
|
|
metadataIndex(0, blobSize),
|
|
]);
|
|
|
|
const customAttributeRow = ({
|
|
value,
|
|
options,
|
|
parentIndexSize,
|
|
typeIndexSize,
|
|
blobSize,
|
|
row,
|
|
}: {
|
|
readonly value: number;
|
|
readonly options: ManagedPeFixtureOptions;
|
|
readonly parentIndexSize: 2 | 4;
|
|
readonly typeIndexSize: MetadataIndexSize;
|
|
readonly blobSize: MetadataIndexSize;
|
|
readonly row: number;
|
|
}): Buffer =>
|
|
Buffer.concat([
|
|
parentIndexSize === 2
|
|
? u16(
|
|
options.malformedCustomAttributeRows?.includes(row)
|
|
? 0xffff
|
|
: (1 << 5) | 14,
|
|
)
|
|
: u32(
|
|
options.malformedCustomAttributeRows?.includes(row)
|
|
? 0xffff_ffff
|
|
: (1 << 5) | 14,
|
|
),
|
|
metadataIndex(
|
|
options.customAttributeTypeRaw ?? (1 << 3) | 3,
|
|
typeIndexSize,
|
|
),
|
|
metadataIndex(value, blobSize),
|
|
]);
|
|
|
|
const manifestResourceRow = (
|
|
name: number,
|
|
stringSize: MetadataIndexSize,
|
|
implementationSize: MetadataIndexSize,
|
|
implementationRaw: number,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u32(0),
|
|
u32(2),
|
|
metadataIndex(name, stringSize),
|
|
metadataIndex(implementationRaw, implementationSize),
|
|
]);
|
|
|
|
const exportedTypeRow = (
|
|
exported: { readonly name: number; readonly namespace: number },
|
|
flags: number,
|
|
stringSize: MetadataIndexSize,
|
|
implementationSize: MetadataIndexSize,
|
|
implementationRaw: number,
|
|
): Buffer =>
|
|
Buffer.concat([
|
|
u32(flags),
|
|
u32(0),
|
|
metadataIndex(exported.name, stringSize),
|
|
metadataIndex(exported.namespace, stringSize),
|
|
metadataIndex(implementationRaw, implementationSize),
|
|
]);
|
|
|
|
const fixedStringAttributeBlob = (value: string): Buffer => {
|
|
const bytes = Buffer.from(textEncoder.encode(value));
|
|
if (bytes.length > 0x7f)
|
|
throw new RangeError("Managed PE fixture target framework is too long");
|
|
return Buffer.concat([Buffer.from([1, 0, bytes.length]), bytes, u16(0)]);
|
|
};
|
|
|
|
/**
|
|
* Lay out ECMA-335 #US items (compressed length, UTF-16LE, terminal flag byte)
|
|
* and return the `ldstr` token that names each value.
|
|
*/
|
|
// ECMA-335 II.24.2.4 terminal flag: 1 when a code unit has a high byte or a
|
|
// low byte in 0x01-0x08, 0x0E-0x1F, 0x27, 0x2D, or 0x7F.
|
|
const userStringFlag = (text: Buffer): number => {
|
|
for (let index = 0; index < text.length; index += 2) {
|
|
const unit = text.readUInt16LE(index);
|
|
if (
|
|
unit > 0xff ||
|
|
(unit >= 0x01 && unit <= 0x08) ||
|
|
(unit >= 0x0e && unit <= 0x1f) ||
|
|
unit === 0x27 ||
|
|
unit === 0x2d ||
|
|
unit === 0x7f
|
|
)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
/** A #US heap whose items carry the ECMA-335 terminal flag, and their tokens. */
|
|
export const fixtureUserStringHeap = (
|
|
values: readonly string[],
|
|
): { readonly heap: Buffer; readonly tokens: readonly string[] } => {
|
|
const items: Buffer[] = [Buffer.from([0])];
|
|
const tokens: string[] = [];
|
|
let size = 1;
|
|
for (const value of values) {
|
|
const text = Buffer.from(value, "utf16le");
|
|
const length = text.length + 1;
|
|
if (length > 0x3fff)
|
|
throw new RangeError("Managed PE fixture user string is too long");
|
|
const prefix =
|
|
length < 0x80
|
|
? Buffer.from([length])
|
|
: Buffer.from([0x80 | (length >> 8), length & 0xff]);
|
|
tokens.push(`0x70${size.toString(16).padStart(6, "0")}`);
|
|
const item = Buffer.concat([
|
|
prefix,
|
|
text,
|
|
Buffer.from([userStringFlag(text)]),
|
|
]);
|
|
items.push(item);
|
|
size += item.length;
|
|
}
|
|
return { heap: Buffer.concat(items), tokens };
|
|
};
|
|
|
|
const metadataStreamHeader = (
|
|
relativeOffset: number,
|
|
size: number,
|
|
name: string,
|
|
): Buffer => {
|
|
const named = cString(name);
|
|
const paddedName = Buffer.alloc(align4(named.length));
|
|
named.copy(paddedName);
|
|
return Buffer.concat([u32(relativeOffset), u32(size), paddedName]);
|
|
};
|
|
|
|
const metadataRoot = (
|
|
{
|
|
tables,
|
|
strings,
|
|
guid,
|
|
blob,
|
|
userStrings,
|
|
}: {
|
|
readonly tables: Buffer;
|
|
readonly strings: Buffer;
|
|
readonly guid: Buffer;
|
|
readonly blob: Buffer;
|
|
readonly userStrings?: Buffer | undefined;
|
|
},
|
|
tableStreamName: "#~" | "#-" = "#~",
|
|
): Buffer => {
|
|
const streams: readonly (readonly [string, Buffer])[] = [
|
|
[tableStreamName, tables],
|
|
["#Strings", strings],
|
|
...(userStrings === undefined ? [] : [["#US", userStrings] as const]),
|
|
["#GUID", guid],
|
|
["#Blob", blob],
|
|
];
|
|
const version = Buffer.from("v4.0.30319\0\0", "utf8");
|
|
const header = Buffer.concat([
|
|
u32(0x424a_5342),
|
|
u16(1),
|
|
u16(1),
|
|
u32(0),
|
|
u32(version.length),
|
|
version,
|
|
u16(0),
|
|
u16(streams.length),
|
|
]);
|
|
const streamHeaderSize = streams.reduce(
|
|
(total, [name]) => total + metadataStreamHeader(0, 0, name).length,
|
|
0,
|
|
);
|
|
let offset = align4(header.length + streamHeaderSize);
|
|
const placed = streams.map(([name, data]) => {
|
|
const at = offset;
|
|
offset = align4(offset + data.length);
|
|
return { name, data, at };
|
|
});
|
|
const headers = Buffer.concat(
|
|
placed.map(({ name, data, at }) =>
|
|
metadataStreamHeader(at, data.length, name),
|
|
),
|
|
);
|
|
const root = Buffer.alloc(offset);
|
|
header.copy(root, 0);
|
|
headers.copy(root, header.length);
|
|
for (const { data, at } of placed) data.copy(root, at);
|
|
return root;
|
|
};
|
|
|
|
const tablesStream = (
|
|
rows: ReadonlyMap<number, readonly Buffer[]>,
|
|
extraValidMask: bigint,
|
|
heapSizes: number,
|
|
extraData?: number,
|
|
): Buffer => {
|
|
let valid = extraValidMask;
|
|
for (const table of rows.keys()) valid |= 1n << BigInt(table);
|
|
const header = Buffer.alloc(24);
|
|
header.writeUInt8(2, 4);
|
|
header.writeUInt8(heapSizes | (extraData === undefined ? 0 : 0x40), 6);
|
|
header.writeUInt8(1, 7);
|
|
header.writeBigUInt64LE(valid, 8);
|
|
const counts: Buffer[] = [];
|
|
for (let index = 0; index <= 44; index += 1) {
|
|
const tableRows = rows.get(index);
|
|
if (tableRows === undefined) continue;
|
|
counts.push(u32(tableRows.length));
|
|
}
|
|
const data: Buffer[] = [];
|
|
for (let index = 0; index <= 44; index += 1) {
|
|
const tableRows = rows.get(index);
|
|
if (tableRows === undefined) continue;
|
|
data.push(...tableRows);
|
|
}
|
|
return Buffer.concat([
|
|
header,
|
|
...counts,
|
|
...(extraData === undefined ? [] : [u32(extraData)]),
|
|
...data,
|
|
]);
|
|
};
|
|
|
|
interface FixtureMetadataRowContext {
|
|
readonly options: ManagedPeFixtureOptions;
|
|
readonly methodRva: number;
|
|
readonly referenceStringIndexes: readonly number[];
|
|
readonly indexes: {
|
|
readonly moduleName: number;
|
|
readonly assemblyName: number;
|
|
readonly attributeName: number;
|
|
readonly attributeNamespace: number;
|
|
readonly constructorName: number;
|
|
readonly typeName: number;
|
|
readonly additionalTypeNames: readonly number[];
|
|
readonly typeNamespace: number;
|
|
readonly fieldName: number;
|
|
readonly methodName: number;
|
|
readonly pinvokeModuleName: number | null;
|
|
readonly pinvokeImportName: number | null;
|
|
readonly resourceName: number;
|
|
readonly exportedTypes: readonly {
|
|
readonly name: number;
|
|
readonly namespace: number;
|
|
}[];
|
|
readonly typeReferences: readonly {
|
|
readonly name: number;
|
|
readonly namespace: number;
|
|
readonly enclosingRow?: number;
|
|
}[];
|
|
readonly tokenBlob: number;
|
|
readonly fieldSignature: number;
|
|
readonly methodSignature: number;
|
|
readonly constructorSignature: number;
|
|
readonly attributeBlob: number;
|
|
readonly typeSpecSignatures: readonly number[];
|
|
readonly methodSpecInstantiation: number;
|
|
};
|
|
readonly heapSizes: {
|
|
readonly strings: MetadataIndexSize;
|
|
readonly blobs: MetadataIndexSize;
|
|
readonly guids: MetadataIndexSize;
|
|
};
|
|
}
|
|
|
|
interface FixtureIndexWidths {
|
|
readonly table: (index: number) => MetadataIndexSize;
|
|
readonly coded: (
|
|
tables: readonly number[],
|
|
tagBits: number,
|
|
) => MetadataIndexSize;
|
|
readonly resolutionScope: MetadataIndexSize;
|
|
readonly typeDefOrRef: MetadataIndexSize;
|
|
readonly memberRefParent: MetadataIndexSize;
|
|
readonly hasCustomAttribute: MetadataIndexSize;
|
|
readonly customAttributeType: MetadataIndexSize;
|
|
readonly implementation: MetadataIndexSize;
|
|
}
|
|
|
|
const repeatedRows = (
|
|
count: number,
|
|
createRow: () => Buffer,
|
|
): readonly Buffer[] => Array.from({ length: count }, createRow);
|
|
|
|
const customAttributeRows = (
|
|
{
|
|
options,
|
|
indexes,
|
|
heapSizes,
|
|
}: Pick<FixtureMetadataRowContext, "options" | "indexes" | "heapSizes">,
|
|
widths: FixtureIndexWidths,
|
|
): readonly Buffer[] =>
|
|
options.targetFramework === null
|
|
? []
|
|
: Array.from({ length: options.customAttributeRowCount ?? 1 }, (_, index) =>
|
|
customAttributeRow({
|
|
value: indexes.attributeBlob,
|
|
options,
|
|
parentIndexSize: widths.hasCustomAttribute,
|
|
typeIndexSize: widths.customAttributeType,
|
|
blobSize: heapSizes.blobs,
|
|
row: index + 1,
|
|
}),
|
|
);
|
|
|
|
const exportedTypeTable = (
|
|
options: ManagedPeFixtureOptions,
|
|
indexes: FixtureMetadataRowContext["indexes"],
|
|
heapSizes: FixtureMetadataRowContext["heapSizes"],
|
|
widths: FixtureIndexWidths,
|
|
): readonly (readonly [number, readonly Buffer[]])[] =>
|
|
indexes.exportedTypes.length === 0
|
|
? []
|
|
: [
|
|
[
|
|
39,
|
|
indexes.exportedTypes.map((exported, index) =>
|
|
exportedTypeRow(
|
|
exported,
|
|
options.exportedTypes?.[index]?.flags ?? 0,
|
|
heapSizes.strings,
|
|
widths.implementation,
|
|
options.exportedTypes?.[index]?.implementationRaw ?? 0,
|
|
),
|
|
),
|
|
],
|
|
];
|
|
|
|
const typeRefTable = (
|
|
options: ManagedPeFixtureOptions,
|
|
indexes: FixtureMetadataRowContext["indexes"],
|
|
heapSizes: FixtureMetadataRowContext["heapSizes"],
|
|
widths: FixtureIndexWidths,
|
|
): readonly [number, readonly Buffer[]] => [
|
|
1,
|
|
[
|
|
typeRefRow(
|
|
indexes.attributeName,
|
|
indexes.attributeNamespace,
|
|
heapSizes.strings,
|
|
metadataIndex(options.typeRefScopeRaw ?? 0, widths.resolutionScope),
|
|
),
|
|
...indexes.typeReferences.map(({ name, namespace, enclosingRow }) =>
|
|
typeRefRow(
|
|
name,
|
|
namespace,
|
|
heapSizes.strings,
|
|
metadataIndex(
|
|
enclosingRow === undefined ? 0 : (enclosingRow << 2) | 3,
|
|
widths.resolutionScope,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
];
|
|
|
|
const nestedClassTable = (
|
|
options: ManagedPeFixtureOptions,
|
|
widths: FixtureIndexWidths,
|
|
): readonly (readonly [number, readonly Buffer[]])[] =>
|
|
(options.nestedTypes?.length ?? 0) === 0
|
|
? []
|
|
: [
|
|
[
|
|
41,
|
|
(options.nestedTypes ?? []).map(([nested, enclosing]) =>
|
|
Buffer.concat([
|
|
metadataIndex(nested, widths.table(2)),
|
|
metadataIndex(enclosing, widths.table(2)),
|
|
]),
|
|
),
|
|
],
|
|
];
|
|
|
|
const specTables = (
|
|
options: ManagedPeFixtureOptions,
|
|
indexes: FixtureMetadataRowContext["indexes"],
|
|
heapSizes: FixtureMetadataRowContext["heapSizes"],
|
|
widths: FixtureIndexWidths,
|
|
): readonly (readonly [number, readonly Buffer[]])[] => [
|
|
...(indexes.typeSpecSignatures.length === 0
|
|
? []
|
|
: ([
|
|
[
|
|
27,
|
|
indexes.typeSpecSignatures.map((signature) =>
|
|
metadataIndex(signature, heapSizes.blobs),
|
|
),
|
|
],
|
|
] as const)),
|
|
...(options.methodSpecMethods === undefined
|
|
? []
|
|
: ([
|
|
[
|
|
43,
|
|
options.methodSpecMethods.map((method) =>
|
|
Buffer.concat([
|
|
metadataIndex(method, widths.coded([6, 10], 1)),
|
|
metadataIndex(indexes.methodSpecInstantiation, heapSizes.blobs),
|
|
]),
|
|
),
|
|
],
|
|
] as const)),
|
|
];
|
|
|
|
const fixtureIndexWidths = ({
|
|
options,
|
|
referenceStringIndexes,
|
|
indexes,
|
|
}: FixtureMetadataRowContext): FixtureIndexWidths => {
|
|
const rowCounts = new Map<number, number>([
|
|
[0, options.moduleRowCount ?? 1],
|
|
[1, indexes.typeReferences.length + 1],
|
|
[2, indexes.additionalTypeNames.length + 1],
|
|
[4, 1],
|
|
[6, options.methodRowCount ?? 1],
|
|
[8, 0],
|
|
[10, 1],
|
|
[
|
|
12,
|
|
options.targetFramework === null
|
|
? 0
|
|
: (options.customAttributeRowCount ?? 1),
|
|
],
|
|
...(indexes.pinvokeModuleName === null ? [] : ([[26, 1]] as const)),
|
|
...(indexes.pinvokeImportName === null ? [] : ([[28, 1]] as const)),
|
|
[27, indexes.typeSpecSignatures.length],
|
|
[32, options.assemblyRowCount ?? 1],
|
|
[35, referenceStringIndexes.length],
|
|
[39, indexes.exportedTypes.length],
|
|
[40, 1],
|
|
[41, options.nestedTypes?.length ?? 0],
|
|
[43, options.methodSpecMethods?.length ?? 0],
|
|
]);
|
|
const count = (table: number): number => rowCounts.get(table) ?? 0;
|
|
const table = (index: number): MetadataIndexSize => indexSize(count(index));
|
|
const coded = (
|
|
tables: readonly number[],
|
|
tagBits: number,
|
|
): MetadataIndexSize => codedIndexSize(tables.map(count), tagBits);
|
|
return {
|
|
table,
|
|
coded,
|
|
resolutionScope: coded([0, 26, 35, 1], 2),
|
|
typeDefOrRef: coded([2, 1, 27], 2),
|
|
memberRefParent: coded([2, 1, 26, 6, 27], 3),
|
|
hasCustomAttribute: coded(
|
|
[
|
|
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
|
|
44, 43,
|
|
],
|
|
5,
|
|
),
|
|
customAttributeType: coded([6, 10], 3),
|
|
implementation: coded([38, 35, 39], 2),
|
|
};
|
|
};
|
|
|
|
const fixtureMetadataRows = ({
|
|
options,
|
|
methodRva,
|
|
referenceStringIndexes,
|
|
indexes,
|
|
heapSizes,
|
|
}: FixtureMetadataRowContext): ReadonlyMap<number, readonly Buffer[]> => {
|
|
const widths = fixtureIndexWidths({
|
|
options,
|
|
methodRva,
|
|
referenceStringIndexes,
|
|
indexes,
|
|
heapSizes,
|
|
});
|
|
const nestedRows = new Set(
|
|
(options.nestedTypes ?? []).map(([nested]) => nested),
|
|
);
|
|
const rows = new Map<number, readonly Buffer[]>([
|
|
[
|
|
0,
|
|
repeatedRows(options.moduleRowCount ?? 1, () =>
|
|
moduleRow(indexes.moduleName, heapSizes.strings, heapSizes.guids),
|
|
),
|
|
],
|
|
typeRefTable(options, indexes, heapSizes, widths),
|
|
[
|
|
2,
|
|
[...indexes.additionalTypeNames, indexes.typeName].map((name, index) =>
|
|
typeDefRow({
|
|
name,
|
|
flags: nestedRows.has(index + 1) ? 0x0010_0002 : 0x0010_0001,
|
|
namespace: indexes.typeNamespace,
|
|
fieldList: 1,
|
|
methodList: 1,
|
|
stringSize: heapSizes.strings,
|
|
typeDefOrRefSize: widths.typeDefOrRef,
|
|
fieldListSize: widths.table(4),
|
|
methodListSize: widths.table(6),
|
|
...(options.extendsRaw === undefined
|
|
? {}
|
|
: { extendsRaw: options.extendsRaw }),
|
|
}),
|
|
),
|
|
],
|
|
[
|
|
4,
|
|
[
|
|
fieldRow(
|
|
indexes.fieldName,
|
|
indexes.fieldSignature,
|
|
heapSizes.strings,
|
|
heapSizes.blobs,
|
|
),
|
|
],
|
|
],
|
|
[
|
|
6,
|
|
repeatedRows(options.methodRowCount ?? 1, () =>
|
|
methodDefRow({
|
|
name: indexes.methodName,
|
|
signature: indexes.methodSignature,
|
|
stringSize: heapSizes.strings,
|
|
blobSize: heapSizes.blobs,
|
|
parameterSize: widths.table(8),
|
|
rva: methodRva,
|
|
flags:
|
|
options.methodFlags ??
|
|
(options.pinvoke === undefined ? 0x0016 : 0x2016),
|
|
}),
|
|
),
|
|
],
|
|
[
|
|
10,
|
|
[
|
|
memberRefRow({
|
|
name: indexes.constructorName,
|
|
signature: indexes.constructorSignature,
|
|
stringSize: heapSizes.strings,
|
|
blobSize: heapSizes.blobs,
|
|
parentSize: widths.memberRefParent,
|
|
...(options.memberRefParentRaw === undefined
|
|
? {}
|
|
: { parentRaw: options.memberRefParentRaw }),
|
|
}),
|
|
],
|
|
],
|
|
[12, customAttributeRows({ options, indexes, heapSizes }, widths)],
|
|
...(indexes.pinvokeModuleName === null
|
|
? []
|
|
: ([
|
|
[26, [moduleRefRow(indexes.pinvokeModuleName, heapSizes.strings)]],
|
|
] as const)),
|
|
...(indexes.pinvokeImportName === null
|
|
? []
|
|
: ([
|
|
[
|
|
28,
|
|
[
|
|
implMapRow(
|
|
indexes.pinvokeImportName,
|
|
1,
|
|
options.pinvoke?.mappingFlags ?? 0x0344,
|
|
heapSizes.strings,
|
|
widths.coded([4, 6], 1),
|
|
widths.table(26),
|
|
options.pinvoke?.memberForwardedRaw ?? (1 << 1) | 1,
|
|
),
|
|
],
|
|
],
|
|
] as const)),
|
|
[
|
|
32,
|
|
repeatedRows(options.assemblyRowCount ?? 1, () =>
|
|
assemblyRow(indexes.assemblyName, heapSizes.strings, heapSizes.blobs),
|
|
),
|
|
],
|
|
[
|
|
35,
|
|
referenceStringIndexes.map((name, index) =>
|
|
assemblyRefRow(
|
|
name,
|
|
indexes.tokenBlob,
|
|
options.malformedAssemblyReferenceRows?.includes(index + 1) ?? false,
|
|
heapSizes.strings,
|
|
heapSizes.blobs,
|
|
),
|
|
),
|
|
],
|
|
...exportedTypeTable(options, indexes, heapSizes, widths),
|
|
...specTables(options, indexes, heapSizes, widths),
|
|
...nestedClassTable(options, widths),
|
|
[
|
|
40,
|
|
[
|
|
manifestResourceRow(
|
|
indexes.resourceName,
|
|
heapSizes.strings,
|
|
widths.implementation,
|
|
options.resourceImplementationRaw ?? 0,
|
|
),
|
|
],
|
|
],
|
|
]);
|
|
return rows;
|
|
};
|
|
|
|
/** Blob indexes for the MemberRef and optional spec rows; defaults leave the heap unchanged. */
|
|
const referenceBlobIndexes = (
|
|
blobs: BlobHeap,
|
|
options: ManagedPeFixtureOptions,
|
|
): Pick<
|
|
FixtureMetadataRowContext["indexes"],
|
|
"constructorSignature" | "typeSpecSignatures" | "methodSpecInstantiation"
|
|
> => ({
|
|
constructorSignature: blobs.add(
|
|
options.memberRefSignature ?? Buffer.from([0x20, 0x01, 0x01, 0x0e]),
|
|
),
|
|
typeSpecSignatures: (options.typeSpecSignatures ?? []).map((signature) =>
|
|
blobs.add(signature),
|
|
),
|
|
// Defaults to GENERICINST with one string type argument.
|
|
methodSpecInstantiation:
|
|
options.methodSpecMethods === undefined
|
|
? 0
|
|
: blobs.add(
|
|
options.methodSpecInstantiation ?? Buffer.from([0x0a, 0x01, 0x0e]),
|
|
),
|
|
});
|
|
|
|
const buildManagedFixtureMetadata = (
|
|
options: ManagedPeFixtureOptions,
|
|
methodRva: number,
|
|
): Buffer => {
|
|
const strings = new StringHeap();
|
|
const blobs = new BlobHeap();
|
|
const moduleName = strings.add("Fixture.dll");
|
|
const assemblyName = strings.add("Fixture.Managed");
|
|
const attributeName = strings.add("TargetFrameworkAttribute");
|
|
const attributeNamespace = strings.add("System.Runtime.Versioning");
|
|
const constructorName = strings.add(options.memberRefName ?? ".ctor");
|
|
const typeName = strings.add(options.typeName ?? "Program");
|
|
const additionalTypeNames = Array.from(
|
|
{ length: options.additionalEmptyTypes ?? 0 },
|
|
(_, index) => strings.add(`Empty${String(index)}`),
|
|
);
|
|
const typeNamespace = strings.add(options.typeNamespace ?? "Fixture");
|
|
const fieldName = strings.add(options.fieldName ?? "counter");
|
|
const methodName = strings.add(options.methodName ?? "Main");
|
|
const pinvokeModuleName =
|
|
options.pinvoke === undefined
|
|
? null
|
|
: strings.add(options.pinvoke.moduleName ?? "user32.dll");
|
|
const pinvokeImportName =
|
|
options.pinvoke === undefined
|
|
? null
|
|
: strings.add(options.pinvoke.importName ?? "MessageBoxW");
|
|
const resourceName = strings.add("Fixture.resources");
|
|
const exportedTypes = (options.exportedTypes ?? []).map(
|
|
({ namespace, name }) => ({
|
|
name: strings.add(name),
|
|
namespace: strings.add(namespace),
|
|
}),
|
|
);
|
|
const typeReferences = (options.typeReferences ?? []).map(
|
|
({ namespace, name, enclosingRow }) => ({
|
|
name: strings.add(name),
|
|
namespace: strings.add(namespace),
|
|
...(enclosingRow === undefined ? {} : { enclosingRow }),
|
|
}),
|
|
);
|
|
const referenceNames = options.references ?? ["System.Runtime"];
|
|
const referenceStringIndexes = referenceNames.map((name) =>
|
|
strings.add(name),
|
|
);
|
|
const tokenBlob = blobs.add(Buffer.from("b77a5c561934e089", "hex"));
|
|
const fieldSignature = blobs.add(
|
|
options.fieldSignature ?? Buffer.from([0x06, 0x08]),
|
|
);
|
|
const methodSignature = blobs.add(
|
|
options.methodSignature ?? Buffer.from([0x00, 0x00, 0x01]),
|
|
);
|
|
const referenceBlobs = referenceBlobIndexes(blobs, options);
|
|
const attributeBlob = blobs.add(
|
|
fixedStringAttributeBlob(
|
|
options.targetFramework === null
|
|
? ".NETCoreApp,Version=v8.0"
|
|
: (options.targetFramework ?? ".NETCoreApp,Version=v8.0"),
|
|
),
|
|
);
|
|
const stringSize = indexSize(strings.size);
|
|
const blobSize = indexSize(blobs.size);
|
|
const guidSize = indexSize(guidHeap(options.mvid).length);
|
|
const rows = fixtureMetadataRows({
|
|
options,
|
|
methodRva,
|
|
referenceStringIndexes,
|
|
indexes: {
|
|
moduleName,
|
|
assemblyName,
|
|
attributeName,
|
|
attributeNamespace,
|
|
constructorName,
|
|
typeName,
|
|
additionalTypeNames,
|
|
typeNamespace,
|
|
fieldName,
|
|
methodName,
|
|
pinvokeModuleName,
|
|
pinvokeImportName,
|
|
resourceName,
|
|
exportedTypes,
|
|
typeReferences,
|
|
tokenBlob,
|
|
fieldSignature,
|
|
methodSignature,
|
|
attributeBlob,
|
|
...referenceBlobs,
|
|
},
|
|
heapSizes: { strings: stringSize, blobs: blobSize, guids: guidSize },
|
|
});
|
|
const heapFlags =
|
|
(stringSize === 4 ? 0x01 : 0) |
|
|
(guidSize === 4 ? 0x02 : 0) |
|
|
(blobSize === 4 ? 0x04 : 0);
|
|
const metadata = metadataRoot({
|
|
tables: tablesStream(
|
|
rows,
|
|
options.metadataValidMaskExtra ?? 0n,
|
|
heapFlags,
|
|
options.metadataExtraData,
|
|
),
|
|
strings: strings.toBuffer(),
|
|
guid: guidHeap(options.mvid),
|
|
blob: blobs.toBuffer(),
|
|
userStrings: options.userStringHeap,
|
|
});
|
|
if (options.corruptMetadataSignature === true) metadata.writeUInt32LE(0, 0);
|
|
return metadata;
|
|
};
|
|
|
|
const buildManagedFixtureImage = (
|
|
options: ManagedPeFixtureOptions,
|
|
buildMetadata: (methodRva: number) => Buffer,
|
|
): Buffer => {
|
|
const resourceData = options.resourceData ?? Buffer.from("resource-data");
|
|
const resourceDirectory = Buffer.concat([
|
|
u32(resourceData.length),
|
|
resourceData,
|
|
]);
|
|
const initialMetadata = buildMetadata(0);
|
|
const resourceOffset = Math.max(
|
|
0x0800,
|
|
Math.ceil((0x0300 + initialMetadata.length) / 0x0200) * 0x0200,
|
|
);
|
|
const resourceRva = 0x2000 + resourceOffset - 0x0200;
|
|
const bodyOffset = Math.max(
|
|
0x0a00,
|
|
Math.ceil((resourceOffset + resourceDirectory.length) / 0x0200) * 0x0200,
|
|
);
|
|
const methodRva = 0x2000 + bodyOffset - 0x0200;
|
|
const metadata = buildMetadata(methodRva);
|
|
const body =
|
|
options.ilBody ??
|
|
Buffer.from([
|
|
0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a,
|
|
0x2a,
|
|
]);
|
|
const legacyReadyToRunOffset = 0x0900;
|
|
const metadataEnd = 0x0300 + metadata.length;
|
|
const resourceEnd = resourceOffset + resourceDirectory.length;
|
|
const bodyEnd = bodyOffset + body.length;
|
|
const overlapsReadyToRun = (start: number, end: number): boolean =>
|
|
legacyReadyToRunOffset < end && legacyReadyToRunOffset + 4 > start;
|
|
const readyToRunOffset =
|
|
options.readyToRun === true
|
|
? overlapsReadyToRun(0x0300, metadataEnd) ||
|
|
overlapsReadyToRun(resourceOffset, resourceEnd) ||
|
|
overlapsReadyToRun(bodyOffset, bodyEnd)
|
|
? Math.ceil(Math.max(metadataEnd, resourceEnd, bodyEnd) / 4) * 4
|
|
: legacyReadyToRunOffset
|
|
: null;
|
|
const rawSectionSize =
|
|
Math.ceil(
|
|
Math.max(
|
|
0x0e00,
|
|
0x0100 + metadata.length,
|
|
bodyOffset - 0x0200 + body.length,
|
|
readyToRunOffset === null ? 0 : readyToRunOffset - 0x0200 + 4,
|
|
) / 0x0200,
|
|
) * 0x0200;
|
|
const virtualSectionSize =
|
|
options.virtualSectionSize ?? Math.max(0x1000, rawSectionSize);
|
|
const image = Buffer.alloc(0x0200 + rawSectionSize);
|
|
image.write("MZ", 0, "ascii");
|
|
image.writeUInt32LE(0x80, 0x3c);
|
|
image.writeUInt32LE(0x0000_4550, 0x80);
|
|
const coff = 0x84;
|
|
image.writeUInt16LE(0x014c, coff);
|
|
image.writeUInt16LE(1, coff + 2);
|
|
image.writeUInt16LE(0x00e0, coff + 16);
|
|
image.writeUInt16LE(0x0102, coff + 18);
|
|
const optional = coff + 20;
|
|
image.writeUInt16LE(0x010b, optional);
|
|
image.writeUInt32LE(0x2000, optional + 20);
|
|
image.writeUInt32LE(0x0040_0000, optional + 28);
|
|
image.writeUInt32LE(0x1000, optional + 32);
|
|
image.writeUInt32LE(0x200, optional + 36);
|
|
image.writeUInt32LE(
|
|
Math.ceil((0x2000 + virtualSectionSize) / 0x1000) * 0x1000,
|
|
optional + 56,
|
|
);
|
|
image.writeUInt32LE(0x200, optional + 60);
|
|
image.writeUInt32LE(16, optional + 92);
|
|
image.writeUInt32LE(0x2000, optional + 96 + 14 * 8);
|
|
image.writeUInt32LE(72, optional + 96 + 14 * 8 + 4);
|
|
const section = optional + 0x00e0;
|
|
image.write(".text\0\0\0", section, "ascii");
|
|
image.writeUInt32LE(virtualSectionSize, section + 8);
|
|
image.writeUInt32LE(0x2000, section + 12);
|
|
image.writeUInt32LE(rawSectionSize, section + 16);
|
|
image.writeUInt32LE(0x0200, section + 20);
|
|
image.writeUInt32LE(0x6000_0020, section + 36);
|
|
const cli = 0x0200;
|
|
image.writeUInt32LE(72, cli);
|
|
image.writeUInt16LE(2, cli + 4);
|
|
image.writeUInt16LE(5, cli + 6);
|
|
image.writeUInt32LE(0x2100, cli + 8);
|
|
image.writeUInt32LE(metadata.length, cli + 12);
|
|
image.writeUInt32LE(options.cliFlags ?? 1, cli + 16);
|
|
image.writeUInt32LE(0x0600_0001, cli + 20);
|
|
image.writeUInt32LE(resourceRva, cli + 24);
|
|
image.writeUInt32LE(resourceDirectory.length, cli + 28);
|
|
if (readyToRunOffset !== null) {
|
|
image.writeUInt32LE(0x2000 + readyToRunOffset - 0x0200, cli + 64);
|
|
image.writeUInt32LE(4, cli + 68);
|
|
image.write("RTR\0", readyToRunOffset, "ascii");
|
|
}
|
|
body.copy(image, bodyOffset);
|
|
metadata.copy(image, 0x0300);
|
|
resourceDirectory.copy(image, resourceOffset);
|
|
return image;
|
|
};
|
|
|
|
/** Build a source-owned PE32/CLI fixture without committing compiled binaries. */
|
|
export const buildManagedPeFixture = (
|
|
options: ManagedPeFixtureOptions = {},
|
|
): Buffer =>
|
|
buildManagedFixtureImage(options, (methodRva) =>
|
|
buildManagedFixtureMetadata(options, methodRva),
|
|
);
|
|
|
|
/** Two attribute classes whose pointer-table slots can reorder physical members. */
|
|
export const buildManagedMemberPointerFixture = (
|
|
options: {
|
|
readonly fieldPointers?: readonly number[] | null;
|
|
readonly methodPointers?: readonly number[] | null;
|
|
readonly streamName?: "#~" | "#-";
|
|
} = {},
|
|
): Buffer => {
|
|
const fieldPointers =
|
|
options.fieldPointers === undefined ? [2, 1] : options.fieldPointers;
|
|
const methodPointers =
|
|
options.methodPointers === undefined ? [2, 1] : options.methodPointers;
|
|
// Both instance constructors call System.Attribute::.ctor(), then return.
|
|
const body = Buffer.from([0x1e, 0x02, 0x28, 0x01, 0, 0, 0x0a, 0x2a]);
|
|
const image = buildManagedFixtureImage({ ilBody: body }, (methodRva) => {
|
|
const strings = new StringHeap();
|
|
const blobs = new BlobHeap();
|
|
const moduleName = strings.add("MemberPointers.dll");
|
|
const assemblyName = strings.add("Fixture.MemberPointers");
|
|
const namespace = strings.add("Fixture");
|
|
const typeNames = ["<Module>", "A", "B"].map((name) => strings.add(name));
|
|
const fieldNames = ["firstField", "secondField"].map((name) =>
|
|
strings.add(name),
|
|
);
|
|
const constructorName = strings.add(".ctor");
|
|
const attributeName = strings.add("Attribute");
|
|
const systemNamespace = strings.add("System");
|
|
const runtimeName = strings.add("System.Runtime");
|
|
const fieldSignature = blobs.add(Buffer.from([0x06, 0x08]));
|
|
const constructorSignature = blobs.add(
|
|
Buffer.from([0x20, 0x01, 0x01, 0x0e]),
|
|
);
|
|
const baseConstructorSignature = blobs.add(Buffer.from([0x20, 0, 0x01]));
|
|
const attributeValue = blobs.add(fixedStringAttributeBlob("fixture"));
|
|
const rows = new Map<number, readonly Buffer[]>([
|
|
[0, [moduleRow(moduleName, 2, 2)]],
|
|
// ResolutionScope selects AssemblyRef 1, which contains System.Attribute.
|
|
[
|
|
1,
|
|
[
|
|
Buffer.concat([
|
|
u16((1 << 2) | 2),
|
|
u16(attributeName),
|
|
u16(systemNamespace),
|
|
]),
|
|
],
|
|
],
|
|
[
|
|
2,
|
|
typeNames.map((name, index) =>
|
|
typeDefRow({
|
|
name,
|
|
namespace: index === 0 ? 0 : namespace,
|
|
fieldList: index === 2 ? 2 : 1,
|
|
methodList: index === 2 ? 2 : 1,
|
|
stringSize: 2,
|
|
typeDefOrRefSize: 2,
|
|
fieldListSize: 2,
|
|
methodListSize: 2,
|
|
extendsRaw: index === 0 ? 0 : (1 << 2) | 1,
|
|
flags: index === 0 ? 0 : 0x0010_0001,
|
|
}),
|
|
),
|
|
],
|
|
[4, fieldNames.map((name) => fieldRow(name, fieldSignature, 2, 2))],
|
|
[
|
|
6,
|
|
[1, 2].map(() =>
|
|
methodDefRow({
|
|
name: constructorName,
|
|
signature: constructorSignature,
|
|
stringSize: 2,
|
|
blobSize: 2,
|
|
parameterSize: 2,
|
|
parameterList: 1,
|
|
rva: methodRva,
|
|
flags: 0x1886,
|
|
}),
|
|
),
|
|
],
|
|
[
|
|
10,
|
|
[
|
|
memberRefRow({
|
|
name: constructorName,
|
|
signature: baseConstructorSignature,
|
|
stringSize: 2,
|
|
blobSize: 2,
|
|
parentSize: 2,
|
|
}),
|
|
],
|
|
],
|
|
// The assembly attribute constructor is physical MethodDef row 1.
|
|
[
|
|
12,
|
|
[
|
|
customAttributeRow({
|
|
value: attributeValue,
|
|
options: { customAttributeTypeRaw: (1 << 3) | 2 },
|
|
parentIndexSize: 2,
|
|
typeIndexSize: 2,
|
|
blobSize: 2,
|
|
row: 1,
|
|
}),
|
|
],
|
|
],
|
|
[32, [assemblyRow(assemblyName, 2, 2)]],
|
|
[35, [assemblyRefRow(runtimeName, 0, false, 2, 2)]],
|
|
]);
|
|
if (fieldPointers !== null) rows.set(3, fieldPointers.map(u16));
|
|
if (methodPointers !== null) rows.set(5, methodPointers.map(u16));
|
|
return metadataRoot(
|
|
{
|
|
tables: tablesStream(rows, 0n, 0),
|
|
strings: strings.toBuffer(),
|
|
guid: guidHeap(),
|
|
blob: blobs.toBuffer(),
|
|
},
|
|
options.streamName ?? "#-",
|
|
);
|
|
});
|
|
// The shared PE shell defaults to an executable; this fixture is a DLL.
|
|
image.writeUInt16LE(image.readUInt16LE(0x84 + 18) | 0x2000, 0x84 + 18);
|
|
image.writeUInt32LE(0, 0x0200 + 20);
|
|
return image;
|
|
};
|
|
|
|
/**
|
|
* Two top-level types A and B, each enclosing a nested type named Inner with
|
|
* one method M and one field F; the first Inner also encloses Leaf with a
|
|
* method M. Every method shares the fixture body. Swapping the enclosing
|
|
* types keeps every row but moves each Inner, with its Leaf, under the other
|
|
* top-level type. NestedClass rows pair a nested TypeDef row with its
|
|
* enclosing row and may be replaced to exercise malformed metadata.
|
|
*/
|
|
export const buildManagedNestedTypeFixture = ({
|
|
swapEnclosing = false,
|
|
nestedClassRows,
|
|
literalTopLevelName,
|
|
topLevelTypes = false,
|
|
}: {
|
|
readonly swapEnclosing?: boolean;
|
|
readonly nestedClassRows?: readonly (readonly [number, number])[];
|
|
readonly literalTopLevelName?: string;
|
|
readonly topLevelTypes?: boolean;
|
|
} = {}): Buffer => {
|
|
const image = buildManagedFixtureImage({}, (methodRva) => {
|
|
const strings = new StringHeap();
|
|
const blobs = new BlobHeap();
|
|
const moduleName = strings.add("Nested.dll");
|
|
const assemblyName = strings.add("Fixture.Nested");
|
|
const namespace = strings.add("Fixture");
|
|
const typeNames = [
|
|
"<Module>",
|
|
"A",
|
|
"B",
|
|
"Inner",
|
|
literalTopLevelName ?? (topLevelTypes ? "OtherInner" : "Inner"),
|
|
"Leaf",
|
|
].map((name) => strings.add(name));
|
|
const methodName = strings.add("M");
|
|
const fieldName = strings.add("F");
|
|
const objectName = strings.add("Object");
|
|
const systemNamespace = strings.add("System");
|
|
const runtimeName = strings.add("System.Runtime");
|
|
const methodSignature = blobs.add(Buffer.from([0x20, 0, 0x08]));
|
|
const fieldSignature = blobs.add(Buffer.from([0x06, 0x08]));
|
|
// TypeDef rows 4 and 5 own one method and one field each; row 6 owns a method.
|
|
const memberList = [1, 1, 1, 1, 2, 3];
|
|
const rows = new Map<number, readonly Buffer[]>([
|
|
[0, [moduleRow(moduleName, 2, 2)]],
|
|
[
|
|
1,
|
|
[
|
|
Buffer.concat([
|
|
u16((1 << 2) | 2),
|
|
u16(objectName),
|
|
u16(systemNamespace),
|
|
]),
|
|
],
|
|
],
|
|
[
|
|
2,
|
|
typeNames.map((name, index) =>
|
|
typeDefRow({
|
|
name,
|
|
namespace: index === 1 || index === 2 ? namespace : 0,
|
|
fieldList: Math.min(memberList[index] ?? 1, 3),
|
|
methodList: memberList[index] ?? 1,
|
|
stringSize: 2,
|
|
typeDefOrRefSize: 2,
|
|
fieldListSize: 2,
|
|
methodListSize: 2,
|
|
extendsRaw: index === 0 ? 0 : (1 << 2) | 1,
|
|
flags:
|
|
index <= 2 ||
|
|
topLevelTypes ||
|
|
(index === 4 && literalTopLevelName !== undefined)
|
|
? 0x0010_0001
|
|
: 0x0010_0002,
|
|
}),
|
|
),
|
|
],
|
|
[4, [1, 2].map(() => fieldRow(fieldName, fieldSignature, 2, 2))],
|
|
[
|
|
6,
|
|
[1, 2, 3].map(() =>
|
|
methodDefRow({
|
|
name: methodName,
|
|
signature: methodSignature,
|
|
stringSize: 2,
|
|
blobSize: 2,
|
|
parameterSize: 2,
|
|
parameterList: 1,
|
|
rva: methodRva,
|
|
flags: 0x0086,
|
|
}),
|
|
),
|
|
],
|
|
[
|
|
41,
|
|
(
|
|
nestedClassRows ??
|
|
(topLevelTypes
|
|
? []
|
|
: [
|
|
[4, swapEnclosing ? 3 : 2],
|
|
[5, swapEnclosing ? 2 : 3],
|
|
[6, 4],
|
|
])
|
|
)
|
|
.filter(
|
|
([nested]) => literalTopLevelName === undefined || nested !== 5,
|
|
)
|
|
.map(([nested, enclosing]) =>
|
|
Buffer.concat([u16(nested), u16(enclosing)]),
|
|
),
|
|
],
|
|
[32, [assemblyRow(assemblyName, 2, 2)]],
|
|
[35, [assemblyRefRow(runtimeName, 0, false, 2, 2)]],
|
|
]);
|
|
return metadataRoot({
|
|
tables: tablesStream(rows, 0n, 0),
|
|
strings: strings.toBuffer(),
|
|
guid: guidHeap(),
|
|
blob: blobs.toBuffer(),
|
|
});
|
|
});
|
|
// The shared PE shell defaults to an executable; this fixture is a DLL.
|
|
image.writeUInt16LE(image.readUInt16LE(0x84 + 18) | 0x2000, 0x84 + 18);
|
|
image.writeUInt32LE(0, 0x0200 + 20);
|
|
return image;
|
|
};
|
|
|
|
/** Build a syntactically valid PE32 fixture with no CLI directory. */
|
|
export const buildNativePeFixture = (): Buffer => {
|
|
const image = buildManagedPeFixture();
|
|
const cliDirectory = 0x84 + 20 + 96 + 14 * 8;
|
|
image.writeUInt32LE(0, cliDirectory);
|
|
image.writeUInt32LE(0, cliDirectory + 4);
|
|
return image;
|
|
};
|