fix(managed): retain facts without inventing delta identities
Recognize baseline-dependent #JTD metadata at the shared layout owner before table sizing and decoding. Report unsupported partial metadata across all inspection workflows, preserve admitted PE/CLI and metadata header facts, and keep member identities and inventories unknown without the baseline. A generated PE carrier reproduced complete coverage and a wrong standalone MethodDef token before the fix. Producer and actual CLI/MCP checks now cover all three inspections, normal metadata, retained comparison after source deletion, exact caller selection, source digests, unchanged bytes, and known header facts. Released .NET writer/reader sources establish framing applicability; natural PE occurrence and real CLR/Edit-and-Continue execution were not verified.
This commit is contained in:
1 parent
36f528efcb
commit
fc2dddc1d9
10 files changed
+419
-28
No files matched your search
@@ -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,
|
||||
|
||||
@@ -400,6 +400,7 @@ const managedParseIssueSchema = z.object({
|
||||
"invalid-stream",
|
||||
"missing-stream",
|
||||
"invalid-tables",
|
||||
"unsupported-metadata",
|
||||
"invalid-row",
|
||||
"invalid-heap-index",
|
||||
"invalid-string",
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<typeof prepareManagedMemberRecovery> | 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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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<string, MetadataStream>;
|
||||
readonly version: string;
|
||||
readonly minimalDeltaOffset: number | null;
|
||||
}
|
||||
|
||||
const readMetadataStreams = (
|
||||
@@ -249,6 +253,7 @@ const readMetadataStreams = (
|
||||
);
|
||||
cursor += 4;
|
||||
const streams = new Map<string, MetadataStream>();
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 = <Value>(
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
!(cause instanceof ManagedReaderFailure) ||
|
||||
cause.issue.code === "resource-limit"
|
||||
managedMetadataFailureStatus(cause.issue) === "partial"
|
||||
)
|
||||
throw cause;
|
||||
issues.push(cause.issue);
|
||||
|
||||
@@ -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<typeof parseEvidence>[] = [];
|
||||
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),
|
||||
);
|
||||
},
|
||||
);
|
||||
+78
@@ -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 };
|
||||
};
|
||||
Reference in new issue
Block a user