diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 961ce5b6f2..b35374f2ac 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -206,6 +206,25 @@ reports `resource-limit` with unavailable or partial coverage; omitted facts do not become empty observations. Ambiguous PE section mappings are rejected rather than interpreted according to section order. +Minimal Edit-and-Continue metadata marked by `#JTD` is not a standalone +assembly inventory. The pinned [.NET 9 reader](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/MetadataReader.cs) +uses different reference widths for this mode, and delta handles require the +baseline and prior generations. The released +[metadata serializer](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs) +emits this marker with `#-`; the +[PE writer](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Reflection.Metadata/src/System/Reflection/PortableExecutable/ManagedPEBuilder.cs) +can embed that metadata root. REA reports `unsupported-metadata` at the marker +header before interpreting table rows, preserving the selected path, artifact +digest, parsed metadata version and stream names, and admitted PE/CLI facts. +Metadata status is partial; member and native +declaration inventories are unavailable. Empty collections do not establish +absence, and a requested type name is not declared unmatched. Ordinary +standalone `#~` and `#-` metadata keep their existing interpretation. This does +not add raw `.dmeta` input, baseline attachment or delta reconstruction. +Regression coverage uses authored metadata in a generated PE shell; it does +not establish naturally emitted application PEs, CLR loadability or a real +Edit-and-Continue execution. + ### Selected member inspection Member inspection admits type names, tokens, list ranges/counts, nesting facts, diff --git a/src/domain/managed/managedArtifact.ts b/src/domain/managed/managedArtifact.ts index 994f21484e..e4a4f6cb8a 100644 --- a/src/domain/managed/managedArtifact.ts +++ b/src/domain/managed/managedArtifact.ts @@ -400,6 +400,7 @@ const managedParseIssueSchema = z.object({ "invalid-stream", "missing-stream", "invalid-tables", + "unsupported-metadata", "invalid-row", "invalid-heap-index", "invalid-string", diff --git a/src/dotnet/ManagedArtifactInspector.ts b/src/dotnet/ManagedArtifactInspector.ts index b39d269fd5..d72f083bdd 100644 --- a/src/dotnet/ManagedArtifactInspector.ts +++ b/src/dotnet/ManagedArtifactInspector.ts @@ -26,7 +26,11 @@ import { type ManagedMetadataLayout, } from "./ManagedMetadataLayout.js"; import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js"; -import { ManagedReaderFailure } from "./ManagedReaderFailure.js"; +import { + ManagedReaderFailure, + managedMetadataFailureStatus, + type ManagedMetadataHeaderFacts, +} from "./ManagedReaderFailure.js"; const flagNames = (flags: number): string[] => { const names: string[] = []; @@ -313,6 +317,7 @@ interface PartialMetadataContext { readonly bytes: Buffer; readonly layout: ManagedPeLayout; readonly issue: ManagedParseIssue; + readonly metadataHeader?: ManagedMetadataHeaderFacts | null; } const partialMetadataResult = ({ @@ -320,10 +325,14 @@ const partialMetadataResult = ({ bytes, layout, issue, + metadataHeader, }: PartialMetadataContext): ManagedArtifactInspection => ({ ...base(target, bytes, layout), classification: { - status: issue.code === "resource-limit" ? "managed" : "malformed", + status: + managedMetadataFailureStatus(issue) === "partial" + ? "managed" + : "malformed", container: "pe", runtime_family: runtimeFamilyFromCliHeader(layout.cli), implementation: "unknown", @@ -337,9 +346,9 @@ const partialMetadataResult = ({ ], }, metadata: { - status: issue.code === "resource-limit" ? "partial" : "malformed", - version: null, - stream_names: [], + status: managedMetadataFailureStatus(issue), + version: metadataHeader?.version ?? null, + stream_names: [...(metadataHeader?.streamNames ?? [])], table_row_counts: {}, }, module: null, @@ -350,9 +359,11 @@ const partialMetadataResult = ({ attributes: [], coverage: { state: "partial", issues: [issue] }, limitations: [ - issue.code === "resource-limit" - ? "CLI metadata inspection stopped at its representation budget; omitted facts are unknown." - : "CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.", + issue.code === "unsupported-metadata" + ? issue.detail + : issue.code === "resource-limit" + ? "CLI metadata inspection stopped at its representation budget; omitted facts are unknown." + : "CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.", ], }); @@ -385,6 +396,7 @@ export const inspectManagedArtifactBytes = ( bytes, layout, issue: cause.issue, + metadataHeader: cause.metadataHeader, }), ), ); @@ -445,7 +457,13 @@ export const inspectManagedArtifactBytes = ( if (!(cause instanceof ManagedReaderFailure)) throw cause; return managedArtifactInspectionSchema.parse( admitManagedProjection( - partialMetadataResult({ target, bytes, layout, issue: cause.issue }), + partialMetadataResult({ + target, + bytes, + layout, + issue: cause.issue, + metadataHeader: cause.metadataHeader, + }), ), ); } diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 7c040152cc..b2d604c8b2 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -25,6 +25,8 @@ import { appendManagedParseIssues, ManagedReaderFailure, uniqueManagedParseIssues, + managedMetadataFailureStatus, + type ManagedMetadataHeaderFacts, } from "./ManagedReaderFailure.js"; import { parseTypes, @@ -50,8 +52,14 @@ import { const unavailable = ( target: BinaryTarget, bytes: Buffer, - issue: ManagedParseIssue | null, typeNames: readonly string[] | undefined, + { + issue, + metadataHeader, + }: { + readonly issue: ManagedParseIssue | null; + readonly metadataHeader?: ManagedMetadataHeaderFacts | null; + }, ): ManagedMemberInspection => managedMemberInspectionSchema.parse( admitManagedProjection({ @@ -63,13 +71,8 @@ const unavailable = ( }, module: null, metadata: { - status: - issue === null - ? "absent" - : issue.code === "resource-limit" - ? "partial" - : "malformed", - version: null, + status: issue === null ? "absent" : managedMetadataFailureStatus(issue), + version: metadataHeader?.version ?? null, table_row_counts: {}, }, identity_scope: { @@ -185,7 +188,7 @@ export const inspectManagedMembersBytes = ( ); const pe = readManagedPeLayout(bytes); if (pe.cli === null) - return unavailable(target, bytes, pe.cliIssue, typeNames); + return unavailable(target, bytes, typeNames, { issue: pe.cliIssue }); const issues: ManagedParseIssue[] = []; let recover: ReturnType | undefined; try { @@ -429,7 +432,7 @@ export const inspectManagedMembersBytes = ( cause instanceof ManagedRepresentationFailure ) return recover(cause.issue.offset); - return unavailable(target, bytes, cause.issue, typeNames); + return unavailable(target, bytes, typeNames, cause); } throw cause; } diff --git a/src/dotnet/ManagedMetadataDelta.test.ts b/src/dotnet/ManagedMetadataDelta.test.ts new file mode 100644 index 0000000000..a285568753 --- /dev/null +++ b/src/dotnet/ManagedMetadataDelta.test.ts @@ -0,0 +1,73 @@ +import { expect, it } from "vitest"; + +import { managedMetadataDelta } from "../../tests/fixtures/managedMetadataDelta.js"; +import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js"; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js"; +import { managedPeFixtureTarget } from "./ManagedPe.fixture.js"; +import { + managedFailure, + managedInspectionInputFailure, +} from "./ManagedReaderFailure.js"; + +it("keeps minimal EnC delta metadata unsupported instead of assigning standalone identities", () => { + const { bytes, markerOffset } = managedMetadataDelta(); + const target = managedPeFixtureTarget(bytes); + const issue = { + code: "unsupported-metadata", + scope: "metadata.#JTD", + offset: markerOffset, + detail: expect.stringContaining("baseline"), + }; + const artifact = inspectManagedArtifactBytes(bytes, target); + const members = inspectManagedMembersBytes(bytes, target, { + typeNames: ["Fixture.Program"], + }); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, target); + expect(artifact).toMatchObject({ + classification: { status: "managed", implementation: "unknown" }, + metadata: { + status: "partial", + version: "v4.0.30319", + stream_names: ["#-", "#Strings", "#US", "#GUID", "#Blob", "#JTD"], + table_row_counts: {}, + }, + coverage: { state: "partial", issues: [issue] }, + module: null, + assembly: null, + }); + for (const report of [members, boundaries]) { + expect(report).toMatchObject({ + artifact: { sha256: target.sha256 }, + metadata: { + status: "partial", + version: "v4.0.30319", + table_row_counts: {}, + }, + coverage: { state: "unavailable", issues: [issue] }, + module: null, + identity_scope: { requires_mvid: null }, + }); + } + expect(members.selection).toEqual({ + type_names: ["Fixture.Program"], + unmatched_type_names: [], + }); + expect(members.methods).toEqual([]); + expect(members.types).toEqual([]); + expect(members.fields).toEqual([]); + expect(members.user_strings).toBeNull(); + expect(boundaries.pinvoke_imports).toEqual([]); + expect( + managedInspectionInputFailure( + "inspect_managed_members", + target.path, + managedFailure( + "unsupported-metadata", + "metadata.#JTD", + "Missing baseline", + markerOffset, + ), + ), + ).toBeUndefined(); +}); diff --git a/src/dotnet/ManagedMetadataLayout.ts b/src/dotnet/ManagedMetadataLayout.ts index 083a79f00a..8b4ab09f39 100644 --- a/src/dotnet/ManagedMetadataLayout.ts +++ b/src/dotnet/ManagedMetadataLayout.ts @@ -2,7 +2,10 @@ import { attachManagedDecodeBudget, ManagedDecodeBudget, } from "./ManagedDecodeBudget.js"; -import { managedFailure } from "./ManagedReaderFailure.js"; +import { + managedFailure, + ManagedReaderFailure, +} from "./ManagedReaderFailure.js"; import { computeRowSize, type RowSizeContext, @@ -214,6 +217,7 @@ const tableSize = (index: number, rows: readonly number[]): 2 | 4 => interface ParsedStreams { readonly streams: ReadonlyMap; readonly version: string; + readonly minimalDeltaOffset: number | null; } const readMetadataStreams = ( @@ -249,6 +253,7 @@ const readMetadataStreams = ( ); cursor += 4; const streams = new Map(); + let minimalDeltaOffset: number | null = null; for (let index = 0; index < streamCount; index += 1) { requireRange(bytes, rootOffset, cursor, 8, "metadata.stream-header"); const relativeOffset = bytes.readUInt32LE(cursor); @@ -273,9 +278,10 @@ const readMetadataStreams = ( offset: rootOffset + relativeOffset, size: streamSize, }); + if (named.name === "#JTD") minimalDeltaOffset = rootOffset + cursor; cursor = named.next; } - return { streams, version }; + return { streams, version, minimalDeltaOffset }; }; interface HeapSizes { @@ -442,7 +448,25 @@ export const readManagedMetadataLayout = ( ); const bytes = artifact.subarray(rootOffset, rootOffset + size); const budget = new ManagedDecodeBudget(); - const { streams, version } = readMetadataStreams(bytes, rootOffset, budget); + const { streams, version, minimalDeltaOffset } = readMetadataStreams( + bytes, + rootOffset, + budget, + ); + // Runtime v9.0.0 MetadataReader recognizes #JTD before sizing references. + // Minimal deltas also use baseline coordinates and cumulative heap handles; + // widening local rows alone cannot establish standalone member identities. + if (minimalDeltaOffset !== null) + throw new ManagedReaderFailure( + { + code: "unsupported-metadata", + scope: "metadata.#JTD", + detail: + "Minimal Edit-and-Continue metadata (#JTD) requires its baseline and prior generations; standalone metadata identities and inventories are unknown.", + offset: minimalDeltaOffset, + }, + { metadataHeader: { version, streamNames: [...streams.keys()] } }, + ); const { tableBytes, tablesOffset, heaps, valid } = readTablesHeader( artifact, streams, diff --git a/src/dotnet/ManagedNativeBoundaryInspector.ts b/src/dotnet/ManagedNativeBoundaryInspector.ts index 941c9f0112..90b59c78c9 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.ts @@ -22,6 +22,8 @@ import { import { appendManagedParseIssues, ManagedReaderFailure, + managedMetadataFailureStatus, + type ManagedMetadataHeaderFacts, } from "./ManagedReaderFailure.js"; import { buildNativeBoundaryInspection, @@ -48,9 +50,11 @@ const emptyInspection = ( { cli = null, issues = [], + metadataHeader = null, }: { readonly cli?: ManagedCliHeader | null; readonly issues?: readonly ManagedParseIssue[]; + readonly metadataHeader?: ManagedMetadataHeaderFacts | null; } = {}, ): ManagedNativeBoundaryInspection => { return managedNativeBoundaryInspectionSchema.parse( @@ -63,12 +67,14 @@ const emptyInspection = ( }, module: null, metadata: { - status: issues.some((issue) => issue.code === "resource-limit") + status: issues.some( + (issue) => managedMetadataFailureStatus(issue) === "partial", + ) ? "partial" : classification === "malformed" ? "malformed" : "absent", - version: null, + version: metadataHeader?.version ?? null, table_row_counts: {}, }, identity_scope: { @@ -147,6 +153,7 @@ export const inspectManagedNativeBoundariesBytes = ( return emptyInspection(target, bytes, "malformed", { cli: pe.cli, issues: [cause.issue], + metadataHeader: cause.metadataHeader, }); } try { @@ -190,6 +197,7 @@ export const inspectManagedNativeBoundariesBytes = ( return emptyInspection(target, bytes, "malformed", { cli: pe.cli, issues: [...issues, cause.issue], + metadataHeader: cause.metadataHeader, }); } }; diff --git a/src/dotnet/ManagedReaderFailure.ts b/src/dotnet/ManagedReaderFailure.ts index 641d17da52..d74306d770 100644 --- a/src/dotnet/ManagedReaderFailure.ts +++ b/src/dotnet/ManagedReaderFailure.ts @@ -1,23 +1,43 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; -/** Bounded format failure retained as caller-visible managed coverage. */ +/** Header facts admitted before an unsupported table layout was identified. */ +export interface ManagedMetadataHeaderFacts { + readonly version: string; + readonly streamNames: readonly string[]; +} + +/** Bounded format or applicability failure retained as managed coverage. */ export class ManagedReaderFailure extends Error { + readonly metadataHeader: ManagedMetadataHeaderFacts | null; + constructor( readonly issue: ManagedParseIssue, - options?: ErrorOptions, + options?: ErrorOptions & { + readonly metadataHeader?: ManagedMetadataHeaderFacts; + }, ) { super(issue.detail, options); + this.metadataHeader = options?.metadataHeader ?? null; } } +/** A resource or unsupported layout leaves metadata unknown, not malformed. */ +export const managedMetadataFailureStatus = ( + issue: ManagedParseIssue, +): "partial" | "malformed" => + issue.code === "resource-limit" || issue.code === "unsupported-metadata" + ? "partial" + : "malformed"; + /** Malformed selected metadata is input failure for every inspection workflow. */ export const managedInspectionInputFailure = ( operation: string, path: string, cause: unknown, ): AnalysisInputError | undefined => - cause instanceof ManagedReaderFailure && cause.issue.code !== "resource-limit" + cause instanceof ManagedReaderFailure && + managedMetadataFailureStatus(cause.issue) === "malformed" ? new AnalysisInputError(operation, { cause }, [ { path: ["path"], @@ -46,7 +66,7 @@ export const readManagedValue = ( } catch (cause: unknown) { if ( !(cause instanceof ManagedReaderFailure) || - cause.issue.code === "resource-limit" + managedMetadataFailureStatus(cause.issue) === "partial" ) throw cause; issues.push(cause.issue); diff --git a/tests/acceptance/managedMetadataDelta.test.ts b/tests/acceptance/managedMetadataDelta.test.ts new file mode 100644 index 0000000000..77ed826ce1 --- /dev/null +++ b/tests/acceptance/managedMetadataDelta.test.ts @@ -0,0 +1,147 @@ +import { createHash } from "node:crypto"; +import { readFile, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { parseEvidence } from "../../src/domain/evidence.js"; +import { + managedArtifactInspectionSchema, + managedMemberInspectionSchema, + managedNativeBoundaryInspectionSchema, +} from "../../src/domain/managed/managedArtifact.js"; +import { managedMemberComparisonResultSchema } from "../../src/domain/managed/managedMemberComparison.js"; +import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js"; +import { managedMetadataDelta } from "../fixtures/managedMetadataDelta.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { cliTest } from "../support/cli/cliFixture.js"; + +cliTest( + "keeps EnC baseline-dependent metadata unknown through CLI and stdio MCP, then compares retained sources after deletion", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-delta-"); + const call = await cli.sharedMcpCall!(); + const sources: ReturnType[] = []; + for (const [name, bytes] of [ + ["delta", managedMetadataDelta().bytes], + ["complete", buildManagedPeFixture()], + ] as const) { + const path = join(root, `${name}.dll`); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + await writeFile(path, bytes); + for (const [operation, command] of [ + ["inspect_managed_artifact", "inspect-managed-artifact"], + ["inspect_managed_members", "inspect-managed-members"], + [ + "inspect_managed_native_boundaries", + "inspect-managed-native-boundaries", + ], + ] as const) { + const selected = operation === "inspect_managed_members"; + const output = await cli.sharedCliRun!({ + arguments: [ + command, + path, + ...(selected ? ["--type", "Fixture.Program"] : []), + "--json", + ], + }); + expect(output.exitCode, output.stderr).toBe(0); + const evidence = parseEvidence(output.json); + const schema = + operation === "inspect_managed_artifact" + ? managedArtifactInspectionSchema + : selected + ? managedMemberInspectionSchema + : managedNativeBoundaryInspectionSchema; + const report = schema.parse(evidence.normalized_result); + expect(report.artifact).toMatchObject({ path, sha256 }); + expect(report.metadata.status).toBe( + name === "delta" ? "partial" : "complete", + ); + expect(report.coverage.state).toBe( + name === "complete" + ? "complete" + : selected || operation === "inspect_managed_native_boundaries" + ? "unavailable" + : "partial", + ); + if (name === "delta") { + expect(report.module).toBeNull(); + expect(report.metadata.version).toBe("v4.0.30319"); + expect(report.metadata.table_row_counts).toEqual({}); + if ("identity_scope" in report) + expect(report.identity_scope.requires_mvid).toBeNull(); + expect(report.coverage.issues).toEqual([ + expect.objectContaining({ + code: "unsupported-metadata", + scope: "metadata.#JTD", + detail: expect.stringContaining("baseline"), + }), + ]); + if (operation === "inspect_managed_artifact") { + const artifact = managedArtifactInspectionSchema.parse(report); + expect(artifact.classification).toMatchObject({ + status: "managed", + implementation: "unknown", + }); + expect(artifact.pe.cli).toMatchObject({ + flags: 1, + metadata_rva: expect.any(Number), + }); + expect(artifact.metadata.stream_names).toEqual([ + "#-", + "#Strings", + "#US", + "#GUID", + "#Blob", + "#JTD", + ]); + } + } else expect(report.coverage.issues).toEqual([]); + if (selected) { + const members = managedMemberInspectionSchema.parse(report); + expect(members.selection).toEqual({ + type_names: ["Fixture.Program"], + unmatched_type_names: [], + }); + if (name === "delta") { + expect(members.method_rows).toBeNull(); + expect(members.methods).toEqual([]); + expect(members.types).toEqual([]); + expect(members.fields).toEqual([]); + expect(members.user_strings).toBeNull(); + } else + expect(members.method_rows).toEqual({ + offset: 0, + count: 1, + total: 1, + }); + sources.push(evidence); + } + const response = await call(operation, { + path, + ...(selected ? { type_names: ["Fixture.Program"] } : {}), + }); + expect(response.isError, JSON.stringify(response)).not.toBe(true); + expect( + parseEvidence(response.structuredContent).normalized_result, + ).toEqual(evidence.normalized_result); + } + expect(await readFile(path)).toEqual(bytes); + await rm(path); + } + const compared = await call("compare_managed_members", { + left: sources[0]!, + right: sources[1]!, + }); + expect(compared.isError, JSON.stringify(compared)).not.toBe(true); + const report = managedMemberComparisonResultSchema.parse( + parseEvidence(compared.structuredContent).normalized_result, + ); + expect(report.coverage.status).toBe("partial"); + expect(report.summary).toMatchObject({ added: 0, removed: 0, unknown: 2 }); + expect(report.evidence_links).toEqual( + sources.map(({ evidence_id }) => evidence_id), + ); + }, +); diff --git a/tests/fixtures/managedMetadataDelta.ts b/tests/fixtures/managedMetadataDelta.ts new file mode 100644 index 0000000000..46fa21ce75 --- /dev/null +++ b/tests/fixtures/managedMetadataDelta.ts @@ -0,0 +1,78 @@ +import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js"; +import { readManagedPeLayout } from "../../src/dotnet/ManagedPeReader.js"; + +const align4 = (size: number): number => Math.ceil(size / 4) * 4; + +/** + * Authored minimal EnC metadata carried by an ordinary PE fixture shell. + * Matches runtime v9.0.0 MetadataBuilder/MetadataSizes: #- and empty #JTD, + * wide references despite small local tables, Module and EncMap rows. + * MethodDef physical row 1 maps to baseline token 0x06000002. This does not + * establish CLR loadability or execute an Edit-and-Continue operation. + */ +export const managedMetadataDelta = () => { + const bytes = buildManagedPeFixture({ targetFramework: null }); + const pe = readManagedPeLayout(bytes); + if (pe.cli === null) throw new Error("Fixture omitted CLI metadata"); + const rootOffset = pe.rvaToOffset( + pe.cli.metadata.rva, + pe.cli.metadata.size, + "fixture metadata", + ); + const strings = Buffer.from("\0Delta.dll\0Updated\0"); + const tables = Buffer.alloc(align4(24 + 3 * 4 + 18 + 20 + 2 * 4 + 1)); + tables[4] = 2; + tables[6] = 7; + tables[7] = 1; + tables.writeBigUInt64LE(1n | (1n << 6n) | (1n << 31n), 8); + tables.writeUInt32LE(1, 24); // Module + tables.writeUInt32LE(1, 28); // MethodDef + tables.writeUInt32LE(2, 32); // EncMap + tables.writeUInt16LE(1, 36); // Module.Generation + tables.writeUInt32LE(1, 38); // Module.Name + tables.writeUInt32LE(1, 42); // MVID + tables.writeUInt32LE(2, 46); // EncId + tables.writeUInt32LE(3, 50); // EncBaseId + tables.writeUInt16LE(0x0086, 54 + 6); // MethodDef.Flags + tables.writeUInt32LE(strings.indexOf("Updated"), 54 + 8); + tables.writeUInt32LE(1, 54 + 12); // Signature + tables.writeUInt32LE(1, 54 + 16); // ParamList, four bytes even with no Param rows + tables.writeUInt32LE(0x0000_0001, 74); // EncMap: Module + tables.writeUInt32LE(0x0600_0002, 78); // EncMap: MethodDef in baseline coordinates + const streams = [ + { name: "#-", data: tables }, + { name: "#Strings", data: strings }, + { name: "#US", data: Buffer.from([0]) }, + { name: "#GUID", data: Buffer.alloc(48, 0x41) }, + { name: "#Blob", data: Buffer.from([0, 3, 0, 0, 1]) }, + { name: "#JTD", data: Buffer.alloc(0) }, + ]; + const headerSize = + 32 + + streams.reduce((sum, { name }) => sum + 8 + align4(name.length + 1), 0); + const metadata = Buffer.alloc( + headerSize + + streams.reduce((sum, { data }) => sum + align4(data.length), 0), + ); + metadata.writeUInt32LE(0x424a_5342); + metadata.writeUInt16LE(1, 4); + metadata.writeUInt16LE(1, 6); + metadata.writeUInt32LE(12, 12); + metadata.write("v4.0.30319", 16); + metadata.writeUInt16LE(streams.length, 30); + let header = 32; + let content = headerSize; + let markerOffset = 0; + for (const { name, data } of streams) { + metadata.writeUInt32LE(content, header); + metadata.writeUInt32LE(align4(data.length), header + 4); + metadata.write(name, header + 8); + metadata.set(data, content); + if (name === "#JTD") markerOffset = rootOffset + header; + header += 8 + align4(name.length + 1); + content += align4(data.length); + } + bytes.set(metadata, rootOffset); + bytes.writeUInt32LE(metadata.length, pe.cli.headerOffset + 12); + return { bytes, markerOffset }; +};