refactor(managed): split metadata analysis
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
|
||||
import {
|
||||
createJavaScriptApplicationEdge,
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
javascriptApplicationGraphSchema,
|
||||
type ApplicationGraphEvidence,
|
||||
type ApplicationEdge,
|
||||
type ApplicationNode,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import {
|
||||
@@ -24,13 +21,16 @@ import {
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import {
|
||||
assessManagedGraphOmissions,
|
||||
completeManagedGraphCoverage,
|
||||
managedGraphEvidenceCoverage,
|
||||
managedGraphResultCoverage,
|
||||
managedSourceCoverage,
|
||||
managedSourcePageCoverage,
|
||||
totalManagedGraphOmitted,
|
||||
} from "./managedApplicationGraphCoverage.js";
|
||||
import {
|
||||
addArtifactNode,
|
||||
addArtifactIdentityNodes,
|
||||
addBoundaryNodes,
|
||||
addMemberNodes,
|
||||
} from "./managedApplicationGraphNodes.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
||||
@@ -117,15 +117,7 @@ export type ManagedApplicationGraphResult = z.infer<
|
||||
typeof managedApplicationGraphResultSchema
|
||||
>;
|
||||
|
||||
type ManagedMethod = ManagedMemberInspection["methods"]["items"][number];
|
||||
type ManagedField = ManagedMemberInspection["fields"]["items"][number];
|
||||
type ManagedType = ManagedMemberInspection["types"]["items"][number];
|
||||
type PinvokeImport =
|
||||
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
|
||||
type NativeImplementation =
|
||||
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
|
||||
|
||||
interface ParsedManagedGraphInput {
|
||||
export interface ParsedManagedGraphInput {
|
||||
readonly artifact: {
|
||||
readonly evidence: Evidence;
|
||||
readonly result: ManagedArtifactInspection;
|
||||
@@ -140,9 +132,9 @@ interface ParsedManagedGraphInput {
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface GraphBuildState {
|
||||
export interface GraphBuildState {
|
||||
readonly nodes: ApplicationNode[];
|
||||
readonly edges: ReturnType<typeof createJavaScriptApplicationEdge>[];
|
||||
readonly edges: ApplicationEdge[];
|
||||
readonly artifactSha256: string;
|
||||
readonly artifactPath: string;
|
||||
readonly evidenceLinks: string[];
|
||||
@@ -151,11 +143,6 @@ interface GraphBuildState {
|
||||
readonly fieldNodes: Map<string, ApplicationNode>;
|
||||
}
|
||||
|
||||
interface ContainsEdgeOptions {
|
||||
readonly kind: string;
|
||||
readonly coverage: ApplicationGraphEvidence["coverage"];
|
||||
}
|
||||
|
||||
const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
@@ -306,495 +293,6 @@ const assertSameArtifact = (
|
||||
);
|
||||
};
|
||||
|
||||
const addArtifactNode = (state: GraphBuildState): ApplicationNode => {
|
||||
const node = createJavaScriptApplicationNode({
|
||||
kind: "artifact",
|
||||
identity: {
|
||||
strategy: "content-digest",
|
||||
stability: "global-exact",
|
||||
sha256: state.artifactSha256,
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: basename(state.artifactPath),
|
||||
properties: {
|
||||
format: "pe",
|
||||
source: "managed-application-graph",
|
||||
},
|
||||
evidence: managedEvidence(state, "project-managed-artifact"),
|
||||
},
|
||||
],
|
||||
});
|
||||
state.nodes.push(node);
|
||||
return node;
|
||||
};
|
||||
|
||||
const addArtifactIdentityNodes = (
|
||||
state: GraphBuildState,
|
||||
artifactNode: ApplicationNode,
|
||||
parsed: ParsedManagedGraphInput,
|
||||
): void => {
|
||||
const artifact = parsed.artifact?.result;
|
||||
const artifactCoverage = managedSourceCoverage(
|
||||
parsed.artifact?.result.coverage.state ?? "complete",
|
||||
);
|
||||
if (artifact?.assembly !== undefined && artifact.assembly !== null) {
|
||||
const assembly = createJavaScriptApplicationNode({
|
||||
kind: "managed-assembly",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-assembly",
|
||||
artifact.assembly.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: artifact.assembly.name,
|
||||
properties: {
|
||||
name: artifact.assembly.name,
|
||||
version: artifact.assembly.version,
|
||||
culture: artifact.assembly.culture,
|
||||
public_key_kind: artifact.assembly.public_key.kind,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_artifact",
|
||||
artifactCoverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
state.nodes.push(assembly);
|
||||
addContainsEdge(state, artifactNode, assembly, {
|
||||
kind: "declares-managed-assembly",
|
||||
coverage: artifactCoverage,
|
||||
});
|
||||
}
|
||||
const module =
|
||||
artifact?.module ??
|
||||
parsed.members?.result.module ??
|
||||
parsed.boundaries?.result.module;
|
||||
if (module !== undefined && module !== null) {
|
||||
const moduleCoverage = managedSourceCoverage(moduleCoverageState(parsed));
|
||||
const moduleNode = createJavaScriptApplicationNode({
|
||||
kind: "managed-module",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-module",
|
||||
module.mvid ?? module.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: module.name,
|
||||
properties: {
|
||||
name: module.name,
|
||||
mvid: module.mvid,
|
||||
generation: module.generation,
|
||||
token: module.token,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_artifact",
|
||||
moduleCoverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
state.nodes.push(moduleNode);
|
||||
addContainsEdge(state, artifactNode, moduleNode, {
|
||||
kind: "declares-managed-module",
|
||||
coverage: moduleCoverage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addMemberNodes = (
|
||||
state: GraphBuildState,
|
||||
artifactNode: ApplicationNode,
|
||||
parsed: ParsedManagedGraphInput,
|
||||
limits: ProjectManagedApplicationGraphInput["limits"],
|
||||
) => {
|
||||
const members = parsed.members?.result;
|
||||
if (members === undefined) return { types: 0, methods: 0, fields: 0 };
|
||||
const types = members.types.items.slice(0, limits.max_types);
|
||||
const methods = members.methods.items.slice(0, limits.max_methods);
|
||||
const fields = members.fields.items.slice(0, limits.max_fields);
|
||||
const typeCoverage = managedSourcePageCoverage(
|
||||
members.coverage.state,
|
||||
members.types,
|
||||
"types",
|
||||
);
|
||||
const methodCoverage = managedSourcePageCoverage(
|
||||
members.coverage.state,
|
||||
members.methods,
|
||||
"methods",
|
||||
);
|
||||
const fieldCoverage = managedSourcePageCoverage(
|
||||
members.coverage.state,
|
||||
members.fields,
|
||||
"fields",
|
||||
);
|
||||
for (const type of types) {
|
||||
const node = typeNode(state, type, typeCoverage);
|
||||
state.nodes.push(node);
|
||||
state.typeNodes.set(type.token, node);
|
||||
addContainsEdge(state, artifactNode, node, {
|
||||
kind: "declares-managed-type",
|
||||
coverage: typeCoverage,
|
||||
});
|
||||
}
|
||||
for (const method of methods) {
|
||||
const node = methodNode(state, method, methodCoverage);
|
||||
state.nodes.push(node);
|
||||
state.methodNodes.set(method.token, node);
|
||||
const owner =
|
||||
method.declaring_type_token === null
|
||||
? artifactNode
|
||||
: (state.typeNodes.get(method.declaring_type_token) ?? artifactNode);
|
||||
addContainsEdge(state, owner, node, {
|
||||
kind: "declares-managed-method",
|
||||
coverage: methodCoverage,
|
||||
});
|
||||
}
|
||||
for (const field of fields) {
|
||||
const node = fieldNode(state, field, fieldCoverage);
|
||||
state.nodes.push(node);
|
||||
state.fieldNodes.set(field.token, node);
|
||||
const owner =
|
||||
field.declaring_type_token === null
|
||||
? artifactNode
|
||||
: (state.typeNodes.get(field.declaring_type_token) ?? artifactNode);
|
||||
addContainsEdge(state, owner, node, {
|
||||
kind: "declares-managed-field",
|
||||
coverage: fieldCoverage,
|
||||
});
|
||||
}
|
||||
return {
|
||||
types: types.length,
|
||||
methods: methods.length,
|
||||
fields: fields.length,
|
||||
};
|
||||
};
|
||||
|
||||
const typeNode = (
|
||||
state: GraphBuildState,
|
||||
type: ManagedType,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-type",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-type-token",
|
||||
type.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: type.full_name,
|
||||
properties: {
|
||||
token: type.token,
|
||||
namespace: type.namespace,
|
||||
name: type.name,
|
||||
full_name: type.full_name,
|
||||
flags: type.flags,
|
||||
extends_token: type.extends_token,
|
||||
},
|
||||
evidence: managedEvidence(state, "inspect_managed_members", coverage),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const methodNode = (
|
||||
state: GraphBuildState,
|
||||
method: ManagedMethod,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-method",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-method-token",
|
||||
method.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label:
|
||||
method.declaring_type === null
|
||||
? method.name
|
||||
: `${method.declaring_type}.${method.name}`,
|
||||
properties: {
|
||||
token: method.token,
|
||||
declaring_type_token: method.declaring_type_token,
|
||||
declaring_type: method.declaring_type,
|
||||
name: method.name,
|
||||
rva: method.rva,
|
||||
signature_sha256: method.signature.raw_sha256,
|
||||
signature_status: method.signature.parse_status,
|
||||
body_status: method.body.status,
|
||||
normalized_il_sha256: method.body.normalized_il_sha256,
|
||||
il_size: method.body.il_size,
|
||||
},
|
||||
evidence: managedEvidence(state, "inspect_managed_members", coverage),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fieldNode = (
|
||||
state: GraphBuildState,
|
||||
field: ManagedField,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-field",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-field-token",
|
||||
field.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label:
|
||||
field.declaring_type === null
|
||||
? field.name
|
||||
: `${field.declaring_type}.${field.name}`,
|
||||
properties: {
|
||||
token: field.token,
|
||||
declaring_type_token: field.declaring_type_token,
|
||||
declaring_type: field.declaring_type,
|
||||
name: field.name,
|
||||
flags: field.flags,
|
||||
signature_sha256: field.signature.raw_sha256,
|
||||
signature_status: field.signature.parse_status,
|
||||
},
|
||||
evidence: managedEvidence(state, "inspect_managed_members", coverage),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const addBoundaryNodes = (
|
||||
state: GraphBuildState,
|
||||
artifactNode: ApplicationNode,
|
||||
parsed: ParsedManagedGraphInput,
|
||||
limits: ProjectManagedApplicationGraphInput["limits"],
|
||||
) => {
|
||||
const boundaries = parsed.boundaries?.result;
|
||||
if (boundaries === undefined)
|
||||
return { pinvoke_imports: 0, native_implementations: 0 };
|
||||
const pinvokes = boundaries.pinvoke_imports.items.slice(
|
||||
0,
|
||||
limits.max_pinvoke_imports,
|
||||
);
|
||||
const implementations = boundaries.native_implementations.items.slice(
|
||||
0,
|
||||
limits.max_native_implementations,
|
||||
);
|
||||
const pinvokeCoverage = managedSourcePageCoverage(
|
||||
boundaries.coverage.state,
|
||||
boundaries.pinvoke_imports,
|
||||
"pinvoke_imports",
|
||||
);
|
||||
const implementationCoverage = managedSourcePageCoverage(
|
||||
boundaries.coverage.state,
|
||||
boundaries.native_implementations,
|
||||
"native_implementations",
|
||||
);
|
||||
for (const pinvoke of pinvokes) {
|
||||
const node = pinvokeNode(state, pinvoke, pinvokeCoverage);
|
||||
state.nodes.push(node);
|
||||
const owner =
|
||||
pinvoke.member_token === null
|
||||
? artifactNode
|
||||
: (state.methodNodes.get(pinvoke.member_token) ?? artifactNode);
|
||||
state.edges.push(
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: owner.node_id,
|
||||
target_node_id: node.node_id,
|
||||
relation: "imports",
|
||||
properties: {
|
||||
kind: "managed-pinvoke",
|
||||
import_name: pinvoke.import_name,
|
||||
import_scope_name: pinvoke.import_scope_name,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_native_boundaries",
|
||||
pinvokeCoverage,
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const implementation of implementations) {
|
||||
const node = nativeImplementationNode(
|
||||
state,
|
||||
implementation,
|
||||
implementationCoverage,
|
||||
);
|
||||
state.nodes.push(node);
|
||||
const owner = state.methodNodes.get(implementation.token) ?? artifactNode;
|
||||
addContainsEdge(state, owner, node, {
|
||||
kind: "declares-managed-native-implementation",
|
||||
coverage: implementationCoverage,
|
||||
});
|
||||
}
|
||||
return {
|
||||
pinvoke_imports: pinvokes.length,
|
||||
native_implementations: implementations.length,
|
||||
};
|
||||
};
|
||||
|
||||
const pinvokeNode = (
|
||||
state: GraphBuildState,
|
||||
pinvoke: PinvokeImport,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-pinvoke-import",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-pinvoke-token",
|
||||
pinvoke.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: pinvoke.import_name,
|
||||
properties: {
|
||||
token: pinvoke.token,
|
||||
member_token: pinvoke.member_token,
|
||||
member_name: pinvoke.member_name,
|
||||
import_name: pinvoke.import_name,
|
||||
import_scope_name: pinvoke.import_scope_name,
|
||||
char_set: pinvoke.char_set,
|
||||
call_convention: pinvoke.call_convention,
|
||||
verification: pinvoke.verification,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_native_boundaries",
|
||||
coverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const nativeImplementationNode = (
|
||||
state: GraphBuildState,
|
||||
implementation: NativeImplementation,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-native-implementation",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-native-implementation-token",
|
||||
implementation.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: implementation.name,
|
||||
properties: {
|
||||
token: implementation.token,
|
||||
name: implementation.name,
|
||||
rva: implementation.rva,
|
||||
code_type: implementation.code_type,
|
||||
managed_kind: implementation.managed_kind,
|
||||
pinvoke_declared: implementation.pinvoke_declared,
|
||||
boundary_kind: implementation.boundary_kind,
|
||||
body_interpretation: implementation.body_interpretation,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_native_boundaries",
|
||||
coverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const addContainsEdge = (
|
||||
state: GraphBuildState,
|
||||
source: ApplicationNode,
|
||||
target: ApplicationNode,
|
||||
options: ContainsEdgeOptions,
|
||||
): void => {
|
||||
state.edges.push(
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: source.node_id,
|
||||
target_node_id: target.node_id,
|
||||
relation: "contains",
|
||||
properties: { kind: options.kind },
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"project-managed-contains",
|
||||
options.coverage,
|
||||
),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const artifactLocalIdentity = (
|
||||
artifactSha256: string,
|
||||
namespace: string,
|
||||
key: string,
|
||||
) => ({
|
||||
strategy: "artifact-local-key" as const,
|
||||
stability: "artifact-version" as const,
|
||||
artifact_sha256: artifactSha256,
|
||||
namespace,
|
||||
key,
|
||||
});
|
||||
|
||||
const managedEvidence = (
|
||||
state: GraphBuildState,
|
||||
operation: string,
|
||||
coverage: ApplicationGraphEvidence["coverage"] = completeManagedGraphCoverage(),
|
||||
): ApplicationGraphEvidence => ({
|
||||
authority: "managed-static-analysis",
|
||||
state: "observed",
|
||||
confidence: "exact",
|
||||
artifact: {
|
||||
available: true,
|
||||
artifact_id: `art_${state.artifactSha256}`,
|
||||
sha256: state.artifactSha256,
|
||||
},
|
||||
location: {
|
||||
available: true,
|
||||
value: {
|
||||
kind: "artifact-path",
|
||||
path: graphArtifactPath(state.artifactPath),
|
||||
},
|
||||
},
|
||||
extractor: {
|
||||
name: "rea-dotnet-static",
|
||||
version: "1",
|
||||
operation,
|
||||
executable_sha256: null,
|
||||
},
|
||||
coverage,
|
||||
limitations:
|
||||
coverage.status === "complete"
|
||||
? []
|
||||
: [
|
||||
"The source managed Evidence slice is incomplete; unreturned or unavailable items remain unobserved.",
|
||||
],
|
||||
evidence_ids: state.evidenceLinks,
|
||||
});
|
||||
|
||||
const graphArtifactPath = (path: string): string => {
|
||||
const name = basename(path).replaceAll(/[^A-Za-z0-9._-]/gu, "_");
|
||||
return `managed/${name.length === 0 ? "artifact.pe" : name}`;
|
||||
};
|
||||
|
||||
const moduleCoverageState = (
|
||||
parsed: ParsedManagedGraphInput,
|
||||
): "complete" | "partial" | "unavailable" => {
|
||||
const artifact = parsed.artifact;
|
||||
if (artifact !== null && artifact.result.module !== null)
|
||||
return artifact.result.coverage.state;
|
||||
const members = parsed.members;
|
||||
if (members !== null && members.result.module !== null)
|
||||
return members.result.coverage.state;
|
||||
return parsed.boundaries?.result.coverage.state ?? "complete";
|
||||
};
|
||||
|
||||
const omittedCounts = (
|
||||
parsed: ParsedManagedGraphInput,
|
||||
limits: ProjectManagedApplicationGraphInput["limits"],
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
import { basename } from "node:path";
|
||||
|
||||
import {
|
||||
createJavaScriptApplicationEdge,
|
||||
createJavaScriptApplicationNode,
|
||||
type ApplicationGraphEvidence,
|
||||
type ApplicationNode,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import {
|
||||
managedSourceCoverage,
|
||||
managedSourcePageCoverage,
|
||||
type ManagedGraphProjectionLimits,
|
||||
} from "./managedApplicationGraphCoverage.js";
|
||||
import {
|
||||
type ManagedMemberInspection,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { GraphBuildState, ParsedManagedGraphInput } from "./managedApplicationGraph.js";
|
||||
|
||||
type ManagedMethod = ManagedMemberInspection["methods"]["items"][number];
|
||||
type ManagedField = ManagedMemberInspection["fields"]["items"][number];
|
||||
type ManagedType = ManagedMemberInspection["types"]["items"][number];
|
||||
type PinvokeImport =
|
||||
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
|
||||
type NativeImplementation =
|
||||
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
|
||||
|
||||
/** Options for a managed contains relationship edge. */
|
||||
export interface ContainsEdgeOptions {
|
||||
readonly kind: string;
|
||||
readonly coverage: ApplicationGraphEvidence["coverage"];
|
||||
}
|
||||
|
||||
/** Add the root managed artifact node to the graph state. */
|
||||
export const addArtifactNode = (state: GraphBuildState): ApplicationNode => {
|
||||
const node = createJavaScriptApplicationNode({
|
||||
kind: "artifact",
|
||||
identity: {
|
||||
strategy: "content-digest",
|
||||
stability: "global-exact",
|
||||
sha256: state.artifactSha256,
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: basename(state.artifactPath),
|
||||
properties: {
|
||||
format: "pe",
|
||||
source: "managed-application-graph",
|
||||
},
|
||||
evidence: managedEvidence(state, "project-managed-artifact"),
|
||||
},
|
||||
],
|
||||
});
|
||||
state.nodes.push(node);
|
||||
return node;
|
||||
};
|
||||
|
||||
/** Add managed assembly and module identity nodes for the artifact. */
|
||||
export const addArtifactIdentityNodes = (
|
||||
state: GraphBuildState,
|
||||
artifactNode: ApplicationNode,
|
||||
parsed: ParsedManagedGraphInput,
|
||||
): void => {
|
||||
const artifact = parsed.artifact?.result;
|
||||
const artifactCoverage = managedSourceCoverage(
|
||||
parsed.artifact?.result.coverage.state ?? "complete",
|
||||
);
|
||||
if (artifact?.assembly !== undefined && artifact.assembly !== null) {
|
||||
const assembly = createJavaScriptApplicationNode({
|
||||
kind: "managed-assembly",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-assembly",
|
||||
artifact.assembly.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: artifact.assembly.name,
|
||||
properties: {
|
||||
name: artifact.assembly.name,
|
||||
version: artifact.assembly.version,
|
||||
culture: artifact.assembly.culture,
|
||||
public_key_kind: artifact.assembly.public_key.kind,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_artifact",
|
||||
artifactCoverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
state.nodes.push(assembly);
|
||||
addContainsEdge(state, artifactNode, assembly, {
|
||||
kind: "declares-managed-assembly",
|
||||
coverage: artifactCoverage,
|
||||
});
|
||||
}
|
||||
const module =
|
||||
artifact?.module ??
|
||||
parsed.members?.result.module ??
|
||||
parsed.boundaries?.result.module;
|
||||
if (module !== undefined && module !== null) {
|
||||
const moduleCoverage = managedSourceCoverage(moduleCoverageState(parsed));
|
||||
const moduleNode = createJavaScriptApplicationNode({
|
||||
kind: "managed-module",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-module",
|
||||
module.mvid ?? module.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: module.name,
|
||||
properties: {
|
||||
name: module.name,
|
||||
mvid: module.mvid,
|
||||
generation: module.generation,
|
||||
token: module.token,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_artifact",
|
||||
moduleCoverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
state.nodes.push(moduleNode);
|
||||
addContainsEdge(state, artifactNode, moduleNode, {
|
||||
kind: "declares-managed-module",
|
||||
coverage: moduleCoverage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Add managed type, method, and field nodes from member evidence. */
|
||||
export const addMemberNodes = (
|
||||
state: GraphBuildState,
|
||||
artifactNode: ApplicationNode,
|
||||
parsed: ParsedManagedGraphInput,
|
||||
limits: ManagedGraphProjectionLimits,
|
||||
) => {
|
||||
const members = parsed.members?.result;
|
||||
if (members === undefined) return { types: 0, methods: 0, fields: 0 };
|
||||
const types = members.types.items.slice(0, limits.max_types);
|
||||
const methods = members.methods.items.slice(0, limits.max_methods);
|
||||
const fields = members.fields.items.slice(0, limits.max_fields);
|
||||
const typeCoverage = managedSourcePageCoverage(
|
||||
members.coverage.state,
|
||||
members.types,
|
||||
"types",
|
||||
);
|
||||
const methodCoverage = managedSourcePageCoverage(
|
||||
members.coverage.state,
|
||||
members.methods,
|
||||
"methods",
|
||||
);
|
||||
const fieldCoverage = managedSourcePageCoverage(
|
||||
members.coverage.state,
|
||||
members.fields,
|
||||
"fields",
|
||||
);
|
||||
for (const type of types) {
|
||||
const node = typeNode(state, type, typeCoverage);
|
||||
state.nodes.push(node);
|
||||
state.typeNodes.set(type.token, node);
|
||||
addContainsEdge(state, artifactNode, node, {
|
||||
kind: "declares-managed-type",
|
||||
coverage: typeCoverage,
|
||||
});
|
||||
}
|
||||
for (const method of methods) {
|
||||
const node = methodNode(state, method, methodCoverage);
|
||||
state.nodes.push(node);
|
||||
state.methodNodes.set(method.token, node);
|
||||
const owner =
|
||||
method.declaring_type_token === null
|
||||
? artifactNode
|
||||
: (state.typeNodes.get(method.declaring_type_token) ?? artifactNode);
|
||||
addContainsEdge(state, owner, node, {
|
||||
kind: "declares-managed-method",
|
||||
coverage: methodCoverage,
|
||||
});
|
||||
}
|
||||
for (const field of fields) {
|
||||
const node = fieldNode(state, field, fieldCoverage);
|
||||
state.nodes.push(node);
|
||||
state.fieldNodes.set(field.token, node);
|
||||
const owner =
|
||||
field.declaring_type_token === null
|
||||
? artifactNode
|
||||
: (state.typeNodes.get(field.declaring_type_token) ?? artifactNode);
|
||||
addContainsEdge(state, owner, node, {
|
||||
kind: "declares-managed-field",
|
||||
coverage: fieldCoverage,
|
||||
});
|
||||
}
|
||||
return {
|
||||
types: types.length,
|
||||
methods: methods.length,
|
||||
fields: fields.length,
|
||||
};
|
||||
};
|
||||
|
||||
/** Build a managed type application node. */
|
||||
export const typeNode = (
|
||||
state: GraphBuildState,
|
||||
type: ManagedType,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-type",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-type-token",
|
||||
type.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: type.full_name,
|
||||
properties: {
|
||||
token: type.token,
|
||||
namespace: type.namespace,
|
||||
name: type.name,
|
||||
full_name: type.full_name,
|
||||
flags: type.flags,
|
||||
extends_token: type.extends_token,
|
||||
},
|
||||
evidence: managedEvidence(state, "inspect_managed_members", coverage),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/** Build a managed method application node. */
|
||||
export const methodNode = (
|
||||
state: GraphBuildState,
|
||||
method: ManagedMethod,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-method",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-method-token",
|
||||
method.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label:
|
||||
method.declaring_type === null
|
||||
? method.name
|
||||
: `${method.declaring_type}.${method.name}`,
|
||||
properties: {
|
||||
token: method.token,
|
||||
declaring_type_token: method.declaring_type_token,
|
||||
declaring_type: method.declaring_type,
|
||||
name: method.name,
|
||||
rva: method.rva,
|
||||
signature_sha256: method.signature.raw_sha256,
|
||||
signature_status: method.signature.parse_status,
|
||||
body_status: method.body.status,
|
||||
normalized_il_sha256: method.body.normalized_il_sha256,
|
||||
il_size: method.body.il_size,
|
||||
},
|
||||
evidence: managedEvidence(state, "inspect_managed_members", coverage),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/** Build a managed field application node. */
|
||||
export const fieldNode = (
|
||||
state: GraphBuildState,
|
||||
field: ManagedField,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-field",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-field-token",
|
||||
field.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label:
|
||||
field.declaring_type === null
|
||||
? field.name
|
||||
: `${field.declaring_type}.${field.name}`,
|
||||
properties: {
|
||||
token: field.token,
|
||||
declaring_type_token: field.declaring_type_token,
|
||||
declaring_type: field.declaring_type,
|
||||
name: field.name,
|
||||
flags: field.flags,
|
||||
signature_sha256: field.signature.raw_sha256,
|
||||
signature_status: field.signature.parse_status,
|
||||
},
|
||||
evidence: managedEvidence(state, "inspect_managed_members", coverage),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/** Add P/Invoke import and native implementation boundary nodes. */
|
||||
export const addBoundaryNodes = (
|
||||
state: GraphBuildState,
|
||||
artifactNode: ApplicationNode,
|
||||
parsed: ParsedManagedGraphInput,
|
||||
limits: ManagedGraphProjectionLimits,
|
||||
) => {
|
||||
const boundaries = parsed.boundaries?.result;
|
||||
if (boundaries === undefined)
|
||||
return { pinvoke_imports: 0, native_implementations: 0 };
|
||||
const pinvokes = boundaries.pinvoke_imports.items.slice(
|
||||
0,
|
||||
limits.max_pinvoke_imports,
|
||||
);
|
||||
const implementations = boundaries.native_implementations.items.slice(
|
||||
0,
|
||||
limits.max_native_implementations,
|
||||
);
|
||||
const pinvokeCoverage = managedSourcePageCoverage(
|
||||
boundaries.coverage.state,
|
||||
boundaries.pinvoke_imports,
|
||||
"pinvoke_imports",
|
||||
);
|
||||
const implementationCoverage = managedSourcePageCoverage(
|
||||
boundaries.coverage.state,
|
||||
boundaries.native_implementations,
|
||||
"native_implementations",
|
||||
);
|
||||
for (const pinvoke of pinvokes) {
|
||||
const node = pinvokeNode(state, pinvoke, pinvokeCoverage);
|
||||
state.nodes.push(node);
|
||||
const owner =
|
||||
pinvoke.member_token === null
|
||||
? artifactNode
|
||||
: (state.methodNodes.get(pinvoke.member_token) ?? artifactNode);
|
||||
state.edges.push(
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: owner.node_id,
|
||||
target_node_id: node.node_id,
|
||||
relation: "imports",
|
||||
properties: {
|
||||
kind: "managed-pinvoke",
|
||||
import_name: pinvoke.import_name,
|
||||
import_scope_name: pinvoke.import_scope_name,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_native_boundaries",
|
||||
pinvokeCoverage,
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const implementation of implementations) {
|
||||
const node = nativeImplementationNode(
|
||||
state,
|
||||
implementation,
|
||||
implementationCoverage,
|
||||
);
|
||||
state.nodes.push(node);
|
||||
const owner = state.methodNodes.get(implementation.token) ?? artifactNode;
|
||||
addContainsEdge(state, owner, node, {
|
||||
kind: "declares-managed-native-implementation",
|
||||
coverage: implementationCoverage,
|
||||
});
|
||||
}
|
||||
return {
|
||||
pinvoke_imports: pinvokes.length,
|
||||
native_implementations: implementations.length,
|
||||
};
|
||||
};
|
||||
|
||||
/** Build a managed P/Invoke import application node. */
|
||||
export const pinvokeNode = (
|
||||
state: GraphBuildState,
|
||||
pinvoke: PinvokeImport,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-pinvoke-import",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-pinvoke-token",
|
||||
pinvoke.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: pinvoke.import_name,
|
||||
properties: {
|
||||
token: pinvoke.token,
|
||||
member_token: pinvoke.member_token,
|
||||
member_name: pinvoke.member_name,
|
||||
import_name: pinvoke.import_name,
|
||||
import_scope_name: pinvoke.import_scope_name,
|
||||
char_set: pinvoke.char_set,
|
||||
call_convention: pinvoke.call_convention,
|
||||
verification: pinvoke.verification,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_native_boundaries",
|
||||
coverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/** Build a managed native implementation application node. */
|
||||
export const nativeImplementationNode = (
|
||||
state: GraphBuildState,
|
||||
implementation: NativeImplementation,
|
||||
coverage: ApplicationGraphEvidence["coverage"],
|
||||
): ApplicationNode =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind: "managed-native-implementation",
|
||||
identity: artifactLocalIdentity(
|
||||
state.artifactSha256,
|
||||
"managed-native-implementation-token",
|
||||
implementation.token,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: implementation.name,
|
||||
properties: {
|
||||
token: implementation.token,
|
||||
name: implementation.name,
|
||||
rva: implementation.rva,
|
||||
code_type: implementation.code_type,
|
||||
managed_kind: implementation.managed_kind,
|
||||
pinvoke_declared: implementation.pinvoke_declared,
|
||||
boundary_kind: implementation.boundary_kind,
|
||||
body_interpretation: implementation.body_interpretation,
|
||||
},
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"inspect_managed_native_boundaries",
|
||||
coverage,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/** Add a contains relationship edge between two nodes. */
|
||||
export const addContainsEdge = (
|
||||
state: GraphBuildState,
|
||||
source: ApplicationNode,
|
||||
target: ApplicationNode,
|
||||
options: ContainsEdgeOptions,
|
||||
): void => {
|
||||
state.edges.push(
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: source.node_id,
|
||||
target_node_id: target.node_id,
|
||||
relation: "contains",
|
||||
properties: { kind: options.kind },
|
||||
evidence: managedEvidence(
|
||||
state,
|
||||
"project-managed-contains",
|
||||
options.coverage,
|
||||
),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/** Build an artifact-local key identity for a managed graph entity. */
|
||||
export const artifactLocalIdentity = (
|
||||
artifactSha256: string,
|
||||
namespace: string,
|
||||
key: string,
|
||||
) => ({
|
||||
strategy: "artifact-local-key" as const,
|
||||
stability: "artifact-version" as const,
|
||||
artifact_sha256: artifactSha256,
|
||||
namespace,
|
||||
key,
|
||||
});
|
||||
|
||||
/** Build managed static-analysis evidence for a graph observation. */
|
||||
export const managedEvidence = (
|
||||
state: GraphBuildState,
|
||||
operation: string,
|
||||
coverage: ApplicationGraphEvidence["coverage"] = managedSourceCoverage("complete"),
|
||||
): ApplicationGraphEvidence => ({
|
||||
authority: "managed-static-analysis",
|
||||
state: "observed",
|
||||
confidence: "exact",
|
||||
artifact: {
|
||||
available: true,
|
||||
artifact_id: `art_${state.artifactSha256}`,
|
||||
sha256: state.artifactSha256,
|
||||
},
|
||||
location: {
|
||||
available: true,
|
||||
value: {
|
||||
kind: "artifact-path",
|
||||
path: graphArtifactPath(state.artifactPath),
|
||||
},
|
||||
},
|
||||
extractor: {
|
||||
name: "rea-dotnet-static",
|
||||
version: "1",
|
||||
operation,
|
||||
executable_sha256: null,
|
||||
},
|
||||
coverage,
|
||||
limitations:
|
||||
coverage.status === "complete"
|
||||
? []
|
||||
: [
|
||||
"The source managed Evidence slice is incomplete; unreturned or unavailable items remain unobserved.",
|
||||
],
|
||||
evidence_ids: state.evidenceLinks,
|
||||
});
|
||||
|
||||
/** Sanitize an artifact path into a managed graph location. */
|
||||
export const graphArtifactPath = (path: string): string => {
|
||||
const name = basename(path).replaceAll(/[^A-Za-z0-9._-]/gu, "_");
|
||||
return `managed/${name.length === 0 ? "artifact.pe" : name}`;
|
||||
};
|
||||
|
||||
/** Derive the module coverage state from the best available evidence source. */
|
||||
export const moduleCoverageState = (
|
||||
parsed: ParsedManagedGraphInput,
|
||||
): "complete" | "partial" | "unavailable" =>
|
||||
parsed.artifact !== null && parsed.artifact.result.module !== null
|
||||
? parsed.artifact.result.coverage.state
|
||||
: parsed.members !== null && parsed.members.result.module !== null
|
||||
? parsed.members.result.coverage.state
|
||||
: (parsed.boundaries?.result.coverage.state ?? "complete");
|
||||
@@ -1,16 +1,24 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, parseEvidence } from "./evidence.js";
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import { assessKnownPageCoverage } from "./knownPageCoverage.js";
|
||||
import {
|
||||
buildComparisonCoverage,
|
||||
buildComparisonMatching,
|
||||
buildComparisonSummary,
|
||||
comparisonLimitations,
|
||||
sideManifest,
|
||||
} from "./managedMemberComparisonCoverage.js";
|
||||
import {
|
||||
buildFieldItems,
|
||||
buildMethodItems,
|
||||
keyMembers,
|
||||
sha256,
|
||||
} from "./managedMemberComparisonMatch.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
||||
@@ -176,44 +184,12 @@ export type ManagedMemberComparisonResult = z.infer<
|
||||
typeof managedMemberComparisonResultSchema
|
||||
>;
|
||||
|
||||
type Method = ManagedMemberInspection["methods"]["items"][number];
|
||||
type Field = ManagedMemberInspection["fields"]["items"][number];
|
||||
type MethodItem = ManagedMemberComparisonResult["methods"][number];
|
||||
type FieldItem = ManagedMemberComparisonResult["fields"][number];
|
||||
type MatchBasis = z.infer<typeof matchBasisSchema>;
|
||||
|
||||
/** One authenticated side of a managed member comparison. */
|
||||
export interface ManagedMemberComparisonSide {
|
||||
readonly evidenceId: string;
|
||||
readonly result: ManagedMemberInspection;
|
||||
}
|
||||
|
||||
interface Keyed<Item> {
|
||||
readonly item: Item;
|
||||
readonly exactKey: string | null;
|
||||
readonly structuralKey: string | null;
|
||||
}
|
||||
|
||||
interface MatchedPair<Item> {
|
||||
readonly left: Keyed<Item>;
|
||||
readonly right: Keyed<Item>;
|
||||
readonly basis: MatchBasis;
|
||||
readonly confidence: "exact" | "high";
|
||||
}
|
||||
|
||||
interface Ambiguous<Item> {
|
||||
readonly left: readonly Keyed<Item>[];
|
||||
readonly right: readonly Keyed<Item>[];
|
||||
readonly basis: MatchBasis;
|
||||
}
|
||||
|
||||
const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed comparison canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
/** Compare two parsed managed member observations without name-based matching. */
|
||||
export const compareManagedMembers = (
|
||||
left: ManagedMemberComparisonSide,
|
||||
@@ -224,56 +200,24 @@ export const compareManagedMembers = (
|
||||
const rightMethodPage = assessKnownPageCoverage(right.result.methods);
|
||||
const leftFieldPage = assessKnownPageCoverage(left.result.fields);
|
||||
const rightFieldPage = assessKnownPageCoverage(right.result.fields);
|
||||
const methodMatches = matchMethods(
|
||||
left.result.methods.items.map(keyMethod),
|
||||
right.result.methods.items.map(keyMethod),
|
||||
const { methodMatches, fieldMatches } = keyMembers(
|
||||
left,
|
||||
right,
|
||||
limits.max_candidates,
|
||||
);
|
||||
const fieldMatches = matchFields(
|
||||
left.result.fields.items.map(keyField),
|
||||
right.result.fields.items.map(keyField),
|
||||
limits.max_candidates,
|
||||
);
|
||||
const methodItems = buildMethodItems(methodMatches, {
|
||||
const itemContext = {
|
||||
leftEvidenceId: left.evidenceId,
|
||||
rightEvidenceId: right.evidenceId,
|
||||
leftComplete: leftMethodPage.sourceComplete,
|
||||
rightComplete: rightMethodPage.sourceComplete,
|
||||
leftComplete: leftMethodPage.sourceComplete && leftFieldPage.sourceComplete,
|
||||
rightComplete:
|
||||
rightMethodPage.sourceComplete && rightFieldPage.sourceComplete,
|
||||
limits,
|
||||
});
|
||||
const fieldItems = buildFieldItems(fieldMatches, {
|
||||
leftEvidenceId: left.evidenceId,
|
||||
rightEvidenceId: right.evidenceId,
|
||||
leftComplete: leftFieldPage.sourceComplete,
|
||||
rightComplete: rightFieldPage.sourceComplete,
|
||||
limits,
|
||||
});
|
||||
};
|
||||
const methodItems = buildMethodItems(methodMatches, itemContext);
|
||||
const fieldItems = buildFieldItems(fieldMatches, itemContext);
|
||||
const allItems = [...methodItems.items, ...fieldItems.items];
|
||||
const summary = {
|
||||
unchanged: allItems.filter(({ status }) => status === "unchanged").length,
|
||||
changed: allItems.filter(({ status }) => status === "changed").length,
|
||||
added: allItems.filter(({ status }) => status === "added").length,
|
||||
removed: allItems.filter(({ status }) => status === "removed").length,
|
||||
unknown: allItems.filter(({ status }) => status === "unknown").length,
|
||||
};
|
||||
const matching = {
|
||||
exact_il_signature: methodItems.items.filter(
|
||||
({ match }) => match.basis === "exact-il-signature",
|
||||
).length,
|
||||
structural_method_shape: methodItems.items.filter(
|
||||
({ match }) => match.basis === "structural-method-shape",
|
||||
).length,
|
||||
field_signature: fieldItems.items.filter(
|
||||
({ match }) => match.basis === "field-signature",
|
||||
).length,
|
||||
ambiguous:
|
||||
methodItems.items.filter(({ match }) => match.status === "ambiguous")
|
||||
.length +
|
||||
fieldItems.items.filter(({ match }) => match.status === "ambiguous")
|
||||
.length,
|
||||
unmatched: allItems.filter(({ match }) => match.status === "unmatched")
|
||||
.length,
|
||||
};
|
||||
const summary = buildComparisonSummary(allItems);
|
||||
const matching = buildComparisonMatching(methodItems.items, fieldItems.items);
|
||||
const coverage = buildComparisonCoverage({
|
||||
left: left.result,
|
||||
right: right.result,
|
||||
@@ -293,7 +237,7 @@ export const compareManagedMembers = (
|
||||
coverage.omitted_candidates,
|
||||
);
|
||||
const result = {
|
||||
schema_version: 1,
|
||||
schema_version: 1 as const,
|
||||
comparison_id: `mmc_${sha256({
|
||||
left: left.evidenceId,
|
||||
right: right.evidenceId,
|
||||
@@ -302,10 +246,10 @@ export const compareManagedMembers = (
|
||||
limits,
|
||||
})}`,
|
||||
algorithm: {
|
||||
name: "rea-managed-member-comparison",
|
||||
version: 1,
|
||||
token_identity: "build-local",
|
||||
name_matching: "not-used",
|
||||
name: "rea-managed-member-comparison" as const,
|
||||
version: 1 as const,
|
||||
token_identity: "build-local" as const,
|
||||
name_matching: "not-used" as const,
|
||||
},
|
||||
left: sideManifest(left),
|
||||
right: sideManifest(right),
|
||||
@@ -320,539 +264,4 @@ export const compareManagedMembers = (
|
||||
return managedMemberComparisonResultSchema.parse(result);
|
||||
};
|
||||
|
||||
type PageAssessment = ReturnType<typeof assessKnownPageCoverage>;
|
||||
|
||||
interface ComparisonCoverageInput {
|
||||
readonly left: ManagedMemberInspection;
|
||||
readonly right: ManagedMemberInspection;
|
||||
readonly leftMethodPage: PageAssessment;
|
||||
readonly rightMethodPage: PageAssessment;
|
||||
readonly leftFieldPage: PageAssessment;
|
||||
readonly rightFieldPage: PageAssessment;
|
||||
readonly omittedMethodItems: number;
|
||||
readonly omittedFieldItems: number;
|
||||
readonly omittedCandidates: number;
|
||||
}
|
||||
|
||||
const buildComparisonCoverage = ({
|
||||
left,
|
||||
right,
|
||||
leftMethodPage,
|
||||
rightMethodPage,
|
||||
leftFieldPage,
|
||||
rightFieldPage,
|
||||
omittedMethodItems,
|
||||
omittedFieldItems,
|
||||
omittedCandidates,
|
||||
}: ComparisonCoverageInput): ManagedMemberComparisonResult["coverage"] => {
|
||||
const omittedMethods =
|
||||
omittedMethodItems +
|
||||
leftMethodPage.sourceOmittedCount +
|
||||
rightMethodPage.sourceOmittedCount;
|
||||
const omittedFields =
|
||||
omittedFieldItems +
|
||||
leftFieldPage.sourceOmittedCount +
|
||||
rightFieldPage.sourceOmittedCount;
|
||||
const leftStatus = sideCoverageStatus(
|
||||
left,
|
||||
leftMethodPage.sourceComplete && leftFieldPage.sourceComplete,
|
||||
);
|
||||
const rightStatus = sideCoverageStatus(
|
||||
right,
|
||||
rightMethodPage.sourceComplete && rightFieldPage.sourceComplete,
|
||||
);
|
||||
const unknownInput =
|
||||
left.coverage.state !== "complete" || right.coverage.state !== "complete";
|
||||
const truncated = omittedMethods + omittedFields + omittedCandidates > 0;
|
||||
return {
|
||||
status: unknownInput
|
||||
? "partial"
|
||||
: truncated
|
||||
? "truncated"
|
||||
: leftStatus === "complete" && rightStatus === "complete"
|
||||
? "complete-within-inputs"
|
||||
: "partial",
|
||||
left_status: leftStatus,
|
||||
right_status: rightStatus,
|
||||
omitted_methods: omittedMethods,
|
||||
omitted_fields: omittedFields,
|
||||
omitted_candidates: omittedCandidates,
|
||||
};
|
||||
};
|
||||
|
||||
const sideCoverageStatus = (
|
||||
result: ManagedMemberInspection,
|
||||
pagesComplete: boolean,
|
||||
): ManagedMemberComparisonResult["coverage"]["left_status"] =>
|
||||
result.coverage.state === "unavailable"
|
||||
? "unavailable"
|
||||
: result.coverage.state === "complete" && pagesComplete
|
||||
? "complete"
|
||||
: "partial";
|
||||
|
||||
const sideManifest = (
|
||||
side: ManagedMemberComparisonSide,
|
||||
): ManagedMemberComparisonResult["left"] => ({
|
||||
evidence_id: side.evidenceId,
|
||||
artifact_sha256: side.result.artifact.sha256,
|
||||
mvid: side.result.module?.mvid ?? null,
|
||||
module_name: side.result.module?.name ?? null,
|
||||
metadata_status: side.result.metadata.status,
|
||||
methods_total: side.result.methods.total,
|
||||
fields_total: side.result.fields.total,
|
||||
});
|
||||
|
||||
const keyMethod = (item: Method): Keyed<Method> => ({
|
||||
item,
|
||||
exactKey:
|
||||
item.signature.parse_status === "decoded" &&
|
||||
item.body.status === "present" &&
|
||||
item.body.normalized_il_sha256 !== null
|
||||
? stableKey([
|
||||
"method-exact",
|
||||
item.signature.raw_sha256,
|
||||
item.body.normalized_il_sha256,
|
||||
])
|
||||
: null,
|
||||
structuralKey:
|
||||
item.body.status === "present"
|
||||
? stableKey([
|
||||
"method-structural",
|
||||
item.signature.kind,
|
||||
item.signature.calling_convention,
|
||||
item.signature.generic_parameter_count,
|
||||
item.signature.parameter_count,
|
||||
item.signature.return_type,
|
||||
item.signature.parameter_types,
|
||||
item.body.status,
|
||||
item.body.header_format,
|
||||
item.body.il_size,
|
||||
item.body.max_stack,
|
||||
item.body.init_locals,
|
||||
item.body.opcode_counts,
|
||||
item.body.anchors.map(({ opcode, operand_kind }) => [
|
||||
opcode,
|
||||
operand_kind,
|
||||
]),
|
||||
item.body.exception_regions.map((region) => [
|
||||
region.flags,
|
||||
region.try_length,
|
||||
region.handler_length,
|
||||
region.class_token === null ? null : "type-token",
|
||||
region.filter_offset === null ? null : "filter",
|
||||
]),
|
||||
])
|
||||
: null,
|
||||
});
|
||||
|
||||
const keyField = (item: Field): Keyed<Field> => ({
|
||||
item,
|
||||
exactKey:
|
||||
item.signature.parse_status === "decoded"
|
||||
? stableKey(["field-exact", item.signature.raw_sha256])
|
||||
: null,
|
||||
structuralKey: stableKey([
|
||||
"field-structural",
|
||||
item.signature.kind,
|
||||
item.signature.field_type,
|
||||
item.flags,
|
||||
]),
|
||||
});
|
||||
|
||||
const stableKey = (value: JsonValue): string => sha256(value);
|
||||
|
||||
const matchMethods = (
|
||||
left: readonly Keyed<Method>[],
|
||||
right: readonly Keyed<Method>[],
|
||||
maxCandidates: number,
|
||||
) =>
|
||||
matchByKeys(left, right, maxCandidates, "exact-il-signature", [
|
||||
"structural-method-shape",
|
||||
]);
|
||||
|
||||
const matchFields = (
|
||||
left: readonly Keyed<Field>[],
|
||||
right: readonly Keyed<Field>[],
|
||||
maxCandidates: number,
|
||||
) => matchByKeys(left, right, maxCandidates, "field-signature", []);
|
||||
|
||||
const matchByKeys = <Item>(
|
||||
left: readonly Keyed<Item>[],
|
||||
right: readonly Keyed<Item>[],
|
||||
maxCandidates: number,
|
||||
exactBasis: MatchBasis,
|
||||
fallbackBases: readonly MatchBasis[],
|
||||
): {
|
||||
readonly pairs: readonly MatchedPair<Item>[];
|
||||
readonly ambiguous: readonly Ambiguous<Item>[];
|
||||
readonly leftOnly: readonly Keyed<Item>[];
|
||||
readonly rightOnly: readonly Keyed<Item>[];
|
||||
readonly omittedCandidates: number;
|
||||
} => {
|
||||
const usedLeft = new Set<Keyed<Item>>();
|
||||
const usedRight = new Set<Keyed<Item>>();
|
||||
const pairs: MatchedPair<Item>[] = [];
|
||||
const ambiguous: Ambiguous<Item>[] = [];
|
||||
let omittedCandidates = 0;
|
||||
const rounds: readonly {
|
||||
readonly basis: MatchBasis;
|
||||
readonly key: (item: Keyed<Item>) => string | null;
|
||||
}[] = [
|
||||
{ basis: exactBasis, key: ({ exactKey }) => exactKey },
|
||||
...fallbackBases.map((basis) => ({
|
||||
basis,
|
||||
key: ({ structuralKey }: Keyed<Item>) => structuralKey,
|
||||
})),
|
||||
];
|
||||
for (const round of rounds) {
|
||||
const leftGroups = groupBy(
|
||||
left.filter((item) => !usedLeft.has(item)),
|
||||
round.key,
|
||||
);
|
||||
const rightGroups = groupBy(
|
||||
right.filter((item) => !usedRight.has(item)),
|
||||
round.key,
|
||||
);
|
||||
for (const [key, leftItems] of leftGroups) {
|
||||
const rightItems = rightGroups.get(key);
|
||||
if (rightItems === undefined) continue;
|
||||
if (leftItems.length === 1 && rightItems.length === 1) {
|
||||
const [leftItem] = leftItems;
|
||||
const [rightItem] = rightItems;
|
||||
if (leftItem === undefined || rightItem === undefined) continue;
|
||||
usedLeft.add(leftItem);
|
||||
usedRight.add(rightItem);
|
||||
pairs.push({
|
||||
left: leftItem,
|
||||
right: rightItem,
|
||||
basis: round.basis,
|
||||
confidence:
|
||||
round.basis === "exact-il-signature" ||
|
||||
round.basis === "field-signature"
|
||||
? "exact"
|
||||
: "high",
|
||||
});
|
||||
} else {
|
||||
for (const item of leftItems) usedLeft.add(item);
|
||||
for (const item of rightItems) usedRight.add(item);
|
||||
omittedCandidates += Math.max(
|
||||
0,
|
||||
leftItems.length + rightItems.length - maxCandidates * 2,
|
||||
);
|
||||
ambiguous.push({
|
||||
left: leftItems.slice(0, maxCandidates),
|
||||
right: rightItems.slice(0, maxCandidates),
|
||||
basis: round.basis,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
pairs,
|
||||
ambiguous,
|
||||
leftOnly: left.filter((item) => !usedLeft.has(item)),
|
||||
rightOnly: right.filter((item) => !usedRight.has(item)),
|
||||
omittedCandidates,
|
||||
};
|
||||
};
|
||||
|
||||
const groupBy = <Item>(
|
||||
items: readonly Item[],
|
||||
keyOf: (item: Item) => string | null,
|
||||
): Map<string, readonly Item[]> => {
|
||||
const grouped = new Map<string, Item[]>();
|
||||
for (const item of items) {
|
||||
const key = keyOf(item);
|
||||
if (key === null) continue;
|
||||
const bucket = grouped.get(key) ?? [];
|
||||
bucket.push(item);
|
||||
grouped.set(key, bucket);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
const buildMethodItems = (
|
||||
matches: ReturnType<typeof matchMethods>,
|
||||
context: ComparisonItemContext,
|
||||
): { readonly items: MethodItem[]; readonly omitted: number } => {
|
||||
const items: MethodItem[] = [];
|
||||
for (const pair of matches.pairs) {
|
||||
const dimensions = changedMethodDimensions(pair.left.item, pair.right.item);
|
||||
items.push({
|
||||
item_id: `mmc_method_${sha256({
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
basis: pair.basis,
|
||||
})}`,
|
||||
status: dimensions.length === 0 ? "unchanged" : "changed",
|
||||
left: methodIdentity(pair.left.item),
|
||||
right: methodIdentity(pair.right.item),
|
||||
match: {
|
||||
status: "matched",
|
||||
basis: pair.basis,
|
||||
confidence: pair.confidence,
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
dimensions,
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [],
|
||||
});
|
||||
}
|
||||
for (const ambiguous of matches.ambiguous) {
|
||||
items.push({
|
||||
item_id: `mmc_method_${sha256({
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
basis: ambiguous.basis,
|
||||
})}`,
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: null,
|
||||
match: {
|
||||
status: "ambiguous",
|
||||
basis: ambiguous.basis,
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
|
||||
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
|
||||
},
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [
|
||||
"Multiple managed methods share the same non-name identity key; REA did not guess a token remap.",
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(methodOnlyItem(item.item, context, true));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(methodOnlyItem(item.item, context, false));
|
||||
return limitItems(items, context.limits.max_method_matches);
|
||||
};
|
||||
|
||||
const buildFieldItems = (
|
||||
matches: ReturnType<typeof matchFields>,
|
||||
context: ComparisonItemContext,
|
||||
): { readonly items: FieldItem[]; readonly omitted: number } => {
|
||||
const items: FieldItem[] = [];
|
||||
for (const pair of matches.pairs) {
|
||||
const changed =
|
||||
pair.left.item.signature.raw_sha256 !==
|
||||
pair.right.item.signature.raw_sha256 ||
|
||||
pair.left.item.flags !== pair.right.item.flags;
|
||||
items.push({
|
||||
item_id: `mmc_field_${sha256({
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
})}`,
|
||||
status: changed ? "changed" : "unchanged",
|
||||
left: fieldIdentity(pair.left.item),
|
||||
right: fieldIdentity(pair.right.item),
|
||||
match: {
|
||||
status: "matched",
|
||||
basis: pair.basis,
|
||||
confidence: pair.confidence,
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [],
|
||||
});
|
||||
}
|
||||
for (const ambiguous of matches.ambiguous)
|
||||
items.push({
|
||||
item_id: `mmc_field_${sha256({
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
})}`,
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: null,
|
||||
match: {
|
||||
status: "ambiguous",
|
||||
basis: ambiguous.basis,
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
|
||||
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [
|
||||
"Multiple managed fields share the same signature; REA did not guess a token remap from names.",
|
||||
],
|
||||
});
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(fieldOnlyItem(item.item, context, true));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(fieldOnlyItem(item.item, context, false));
|
||||
return limitItems(items, context.limits.max_field_matches);
|
||||
};
|
||||
|
||||
interface ComparisonItemContext {
|
||||
readonly leftEvidenceId: string;
|
||||
readonly rightEvidenceId: string;
|
||||
readonly leftComplete: boolean;
|
||||
readonly rightComplete: boolean;
|
||||
readonly limits: CompareManagedMembersInput["limits"];
|
||||
}
|
||||
|
||||
const limitItems = <Item>(
|
||||
items: readonly Item[],
|
||||
limit: number,
|
||||
): { readonly items: Item[]; readonly omitted: number } => ({
|
||||
items: items.slice(0, limit),
|
||||
omitted: Math.max(0, items.length - limit),
|
||||
});
|
||||
|
||||
const methodOnlyItem = (
|
||||
item: Method,
|
||||
context: ComparisonItemContext,
|
||||
left: boolean,
|
||||
): MethodItem => {
|
||||
const absenceObserved = left ? context.rightComplete : context.leftComplete;
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
|
||||
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
|
||||
left: left ? methodIdentity(item) : null,
|
||||
right: left ? null : methodIdentity(item),
|
||||
match: {
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
`unknown-within-unobserved-page: The ${left ? "right" : "left"} method page is incomplete, so absence was not observed.`,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const fieldOnlyItem = (
|
||||
item: Field,
|
||||
context: ComparisonItemContext,
|
||||
left: boolean,
|
||||
): FieldItem => {
|
||||
const absenceObserved = left ? context.rightComplete : context.leftComplete;
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
|
||||
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
|
||||
left: left ? fieldIdentity(item) : null,
|
||||
right: left ? null : fieldIdentity(item),
|
||||
match: {
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
`unknown-within-unobserved-page: The ${left ? "right" : "left"} field page is incomplete, so absence was not observed.`,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const methodIdentity = (item: Method): NonNullable<MethodItem["left"]> => ({
|
||||
token: item.token,
|
||||
declaring_type: item.declaring_type,
|
||||
name: item.name,
|
||||
signature_sha256: item.signature.raw_sha256,
|
||||
normalized_il_sha256: item.body.normalized_il_sha256,
|
||||
});
|
||||
|
||||
const fieldIdentity = (item: Field): NonNullable<FieldItem["left"]> => ({
|
||||
token: item.token,
|
||||
declaring_type: item.declaring_type,
|
||||
name: item.name,
|
||||
signature_sha256: item.signature.raw_sha256,
|
||||
});
|
||||
|
||||
const changedMethodDimensions = (
|
||||
left: Method,
|
||||
right: Method,
|
||||
): MethodItem["dimensions"] => {
|
||||
const dimensions: MethodItem["dimensions"] = [];
|
||||
if (left.signature.raw_sha256 !== right.signature.raw_sha256)
|
||||
dimensions.push("signature");
|
||||
if (left.body.normalized_il_sha256 !== right.body.normalized_il_sha256)
|
||||
dimensions.push("cil");
|
||||
if (
|
||||
canonicalize(left.body.opcode_counts) !==
|
||||
canonicalize(right.body.opcode_counts)
|
||||
)
|
||||
dimensions.push("opcode-shape");
|
||||
if (callShape(left.body.anchors) !== callShape(right.body.anchors))
|
||||
dimensions.push("call-shape");
|
||||
if (fieldShape(left.body.anchors) !== fieldShape(right.body.anchors))
|
||||
dimensions.push("field-shape");
|
||||
if (
|
||||
canonicalize(left.body.exception_regions) !==
|
||||
canonicalize(right.body.exception_regions)
|
||||
)
|
||||
dimensions.push("exception-shape");
|
||||
if (left.body.status !== right.body.status) dimensions.push("availability");
|
||||
return dimensions;
|
||||
};
|
||||
|
||||
const callShape = (anchors: Method["body"]["anchors"]): string =>
|
||||
JSON.stringify(
|
||||
anchors
|
||||
.filter(({ operand_kind }) => operand_kind === "method")
|
||||
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
|
||||
);
|
||||
|
||||
const fieldShape = (anchors: Method["body"]["anchors"]): string =>
|
||||
JSON.stringify(
|
||||
anchors
|
||||
.filter(({ operand_kind }) => operand_kind === "field")
|
||||
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
|
||||
);
|
||||
|
||||
const comparisonLimitations = (
|
||||
left: ManagedMemberInspection,
|
||||
right: ManagedMemberInspection,
|
||||
omittedItems: number,
|
||||
omittedCandidates: number,
|
||||
): string[] => {
|
||||
const limitations: string[] = [
|
||||
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
|
||||
"Names are reported as observations but are not used as a matching basis.",
|
||||
];
|
||||
if (!left.methods.complete || !right.methods.complete)
|
||||
limitations.push("At least one method page is incomplete.");
|
||||
if (!left.fields.complete || !right.fields.complete)
|
||||
limitations.push("At least one field page is incomplete.");
|
||||
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
|
||||
limitations.push("At least one managed member observation is partial.");
|
||||
if (omittedItems > 0)
|
||||
limitations.push(
|
||||
`${String(omittedItems)} comparison items were omitted by output limits.`,
|
||||
);
|
||||
if (omittedCandidates > 0)
|
||||
limitations.push(
|
||||
`${String(omittedCandidates)} ambiguous candidates were omitted by candidate limits.`,
|
||||
);
|
||||
return limitations;
|
||||
};
|
||||
|
||||
/** Extract and authenticate a managed member inspection Evidence record. */
|
||||
export const parseManagedMemberEvidence = (
|
||||
evidence: unknown,
|
||||
): {
|
||||
readonly evidenceId: string;
|
||||
readonly result: ManagedMemberInspection;
|
||||
} => {
|
||||
const parsed = parseEvidence(evidence);
|
||||
if (parsed.operation !== "inspect_managed_members")
|
||||
throw new TypeError("Evidence operation is not inspect_managed_members");
|
||||
return {
|
||||
evidenceId: parsed.evidence_id,
|
||||
result: managedMemberInspectionSchema.parse(parsed.normalized_result),
|
||||
};
|
||||
};
|
||||
export { parseManagedMemberEvidence } from "./managedMemberComparisonMatch.js";
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import type {
|
||||
ManagedMemberComparisonResult,
|
||||
ManagedMemberComparisonSide,
|
||||
} from "./managedMemberComparison.js";
|
||||
import type { ManagedMemberInspection } from "./managedArtifact.js";
|
||||
|
||||
export interface PageAssessment {
|
||||
readonly sourceComplete: boolean;
|
||||
readonly sourceOmittedCount: number;
|
||||
}
|
||||
|
||||
interface ComparisonCoverageInput {
|
||||
readonly left: ManagedMemberInspection;
|
||||
readonly right: ManagedMemberInspection;
|
||||
readonly leftMethodPage: PageAssessment;
|
||||
readonly rightMethodPage: PageAssessment;
|
||||
readonly leftFieldPage: PageAssessment;
|
||||
readonly rightFieldPage: PageAssessment;
|
||||
readonly omittedMethodItems: number;
|
||||
readonly omittedFieldItems: number;
|
||||
readonly omittedCandidates: number;
|
||||
}
|
||||
|
||||
export const buildComparisonCoverage = ({
|
||||
left,
|
||||
right,
|
||||
leftMethodPage,
|
||||
rightMethodPage,
|
||||
leftFieldPage,
|
||||
rightFieldPage,
|
||||
omittedMethodItems,
|
||||
omittedFieldItems,
|
||||
omittedCandidates,
|
||||
}: ComparisonCoverageInput): ManagedMemberComparisonResult["coverage"] => {
|
||||
const omittedMethods =
|
||||
omittedMethodItems +
|
||||
leftMethodPage.sourceOmittedCount +
|
||||
rightMethodPage.sourceOmittedCount;
|
||||
const omittedFields =
|
||||
omittedFieldItems +
|
||||
leftFieldPage.sourceOmittedCount +
|
||||
rightFieldPage.sourceOmittedCount;
|
||||
const leftStatus = sideCoverageStatus(
|
||||
left,
|
||||
leftMethodPage.sourceComplete && leftFieldPage.sourceComplete,
|
||||
);
|
||||
const rightStatus = sideCoverageStatus(
|
||||
right,
|
||||
rightMethodPage.sourceComplete && rightFieldPage.sourceComplete,
|
||||
);
|
||||
const unknownInput =
|
||||
left.coverage.state !== "complete" || right.coverage.state !== "complete";
|
||||
const truncated = omittedMethods + omittedFields + omittedCandidates > 0;
|
||||
return {
|
||||
status: unknownInput
|
||||
? "partial"
|
||||
: truncated
|
||||
? "truncated"
|
||||
: leftStatus === "complete" && rightStatus === "complete"
|
||||
? "complete-within-inputs"
|
||||
: "partial",
|
||||
left_status: leftStatus,
|
||||
right_status: rightStatus,
|
||||
omitted_methods: omittedMethods,
|
||||
omitted_fields: omittedFields,
|
||||
omitted_candidates: omittedCandidates,
|
||||
};
|
||||
};
|
||||
|
||||
const sideCoverageStatus = (
|
||||
result: ManagedMemberInspection,
|
||||
pagesComplete: boolean,
|
||||
): ManagedMemberComparisonResult["coverage"]["left_status"] =>
|
||||
result.coverage.state === "unavailable"
|
||||
? "unavailable"
|
||||
: result.coverage.state === "complete" && pagesComplete
|
||||
? "complete"
|
||||
: "partial";
|
||||
|
||||
export const sideManifest = (
|
||||
side: ManagedMemberComparisonSide,
|
||||
): ManagedMemberComparisonResult["left"] => ({
|
||||
evidence_id: side.evidenceId,
|
||||
artifact_sha256: side.result.artifact.sha256,
|
||||
mvid: side.result.module?.mvid ?? null,
|
||||
module_name: side.result.module?.name ?? null,
|
||||
metadata_status: side.result.metadata.status,
|
||||
methods_total: side.result.methods.total,
|
||||
fields_total: side.result.fields.total,
|
||||
});
|
||||
|
||||
export const buildComparisonSummary = (
|
||||
items: readonly (
|
||||
| ManagedMemberComparisonResult["methods"][number]
|
||||
| ManagedMemberComparisonResult["fields"][number]
|
||||
)[],
|
||||
): ManagedMemberComparisonResult["summary"] => ({
|
||||
unchanged: items.filter(({ status }) => status === "unchanged").length,
|
||||
changed: items.filter(({ status }) => status === "changed").length,
|
||||
added: items.filter(({ status }) => status === "added").length,
|
||||
removed: items.filter(({ status }) => status === "removed").length,
|
||||
unknown: items.filter(({ status }) => status === "unknown").length,
|
||||
});
|
||||
|
||||
export const buildComparisonMatching = (
|
||||
methodItems: readonly ManagedMemberComparisonResult["methods"][number][],
|
||||
fieldItems: readonly ManagedMemberComparisonResult["fields"][number][],
|
||||
): ManagedMemberComparisonResult["matching"] => ({
|
||||
exact_il_signature: methodItems.filter(
|
||||
({ match }) => match.basis === "exact-il-signature",
|
||||
).length,
|
||||
structural_method_shape: methodItems.filter(
|
||||
({ match }) => match.basis === "structural-method-shape",
|
||||
).length,
|
||||
field_signature: fieldItems.filter(
|
||||
({ match }) => match.basis === "field-signature",
|
||||
).length,
|
||||
ambiguous:
|
||||
methodItems.filter(({ match }) => match.status === "ambiguous").length +
|
||||
fieldItems.filter(({ match }) => match.status === "ambiguous").length,
|
||||
unmatched: [...methodItems, ...fieldItems].filter(
|
||||
({ match }) => match.status === "unmatched",
|
||||
).length,
|
||||
});
|
||||
|
||||
export const comparisonLimitations = (
|
||||
left: ManagedMemberInspection,
|
||||
right: ManagedMemberInspection,
|
||||
omittedItems: number,
|
||||
omittedCandidates: number,
|
||||
): string[] => {
|
||||
const limitations: string[] = [
|
||||
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
|
||||
"Names are reported as observations but are not used as a matching basis.",
|
||||
];
|
||||
if (!left.methods.complete || !right.methods.complete)
|
||||
limitations.push("At least one method page is incomplete.");
|
||||
if (!left.fields.complete || !right.fields.complete)
|
||||
limitations.push("At least one field page is incomplete.");
|
||||
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
|
||||
limitations.push("At least one managed member observation is partial.");
|
||||
if (omittedItems > 0)
|
||||
limitations.push(
|
||||
`${String(omittedItems)} comparison items were omitted by output limits.`,
|
||||
);
|
||||
if (omittedCandidates > 0)
|
||||
limitations.push(
|
||||
`${String(omittedCandidates)} ambiguous candidates were omitted by candidate limits.`,
|
||||
);
|
||||
return limitations;
|
||||
};
|
||||
@@ -0,0 +1,513 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { parseEvidence } from "./evidence.js";
|
||||
import {
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type {
|
||||
CompareManagedMembersInput,
|
||||
ManagedMemberComparisonResult,
|
||||
ManagedMemberComparisonSide,
|
||||
} from "./managedMemberComparison.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
export const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed comparison canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
type Method = ManagedMemberInspection["methods"]["items"][number];
|
||||
type Field = ManagedMemberInspection["fields"]["items"][number];
|
||||
type MethodItem = ManagedMemberComparisonResult["methods"][number];
|
||||
type FieldItem = ManagedMemberComparisonResult["fields"][number];
|
||||
type MatchBasis = MethodItem["match"]["basis"];
|
||||
|
||||
interface Keyed<Item> {
|
||||
readonly item: Item;
|
||||
readonly exactKey: string | null;
|
||||
readonly structuralKey: string | null;
|
||||
}
|
||||
|
||||
interface MatchedPair<Item> {
|
||||
readonly left: Keyed<Item>;
|
||||
readonly right: Keyed<Item>;
|
||||
readonly basis: MatchBasis;
|
||||
readonly confidence: "exact" | "high";
|
||||
}
|
||||
|
||||
interface Ambiguous<Item> {
|
||||
readonly left: readonly Keyed<Item>[];
|
||||
readonly right: readonly Keyed<Item>[];
|
||||
readonly basis: MatchBasis;
|
||||
}
|
||||
|
||||
export interface ComparisonItemContext {
|
||||
readonly leftEvidenceId: string;
|
||||
readonly rightEvidenceId: string;
|
||||
readonly leftComplete: boolean;
|
||||
readonly rightComplete: boolean;
|
||||
readonly limits: CompareManagedMembersInput["limits"];
|
||||
}
|
||||
|
||||
const keyMethod = (item: Method): Keyed<Method> => ({
|
||||
item,
|
||||
exactKey:
|
||||
item.signature.parse_status === "decoded" &&
|
||||
item.body.status === "present" &&
|
||||
item.body.normalized_il_sha256 !== null
|
||||
? stableKey([
|
||||
"method-exact",
|
||||
item.signature.raw_sha256,
|
||||
item.body.normalized_il_sha256,
|
||||
])
|
||||
: null,
|
||||
structuralKey:
|
||||
item.body.status === "present"
|
||||
? stableKey([
|
||||
"method-structural",
|
||||
item.signature.kind,
|
||||
item.signature.calling_convention,
|
||||
item.signature.generic_parameter_count,
|
||||
item.signature.parameter_count,
|
||||
item.signature.return_type,
|
||||
item.signature.parameter_types,
|
||||
item.body.status,
|
||||
item.body.header_format,
|
||||
item.body.il_size,
|
||||
item.body.max_stack,
|
||||
item.body.init_locals,
|
||||
item.body.opcode_counts,
|
||||
item.body.anchors.map(({ opcode, operand_kind }) => [
|
||||
opcode,
|
||||
operand_kind,
|
||||
]),
|
||||
item.body.exception_regions.map((region) => [
|
||||
region.flags,
|
||||
region.try_length,
|
||||
region.handler_length,
|
||||
region.class_token === null ? null : "type-token",
|
||||
region.filter_offset === null ? null : "filter",
|
||||
]),
|
||||
])
|
||||
: null,
|
||||
});
|
||||
|
||||
const keyField = (item: Field): Keyed<Field> => ({
|
||||
item,
|
||||
exactKey:
|
||||
item.signature.parse_status === "decoded"
|
||||
? stableKey(["field-exact", item.signature.raw_sha256])
|
||||
: null,
|
||||
structuralKey: stableKey([
|
||||
"field-structural",
|
||||
item.signature.kind,
|
||||
item.signature.field_type,
|
||||
item.flags,
|
||||
]),
|
||||
});
|
||||
|
||||
const stableKey = (value: JsonValue): string => sha256(value);
|
||||
|
||||
const matchMethods = (
|
||||
left: readonly Keyed<Method>[],
|
||||
right: readonly Keyed<Method>[],
|
||||
maxCandidates: number,
|
||||
) =>
|
||||
matchByKeys({
|
||||
left,
|
||||
right,
|
||||
maxCandidates,
|
||||
exactBasis: "exact-il-signature",
|
||||
fallbackBases: ["structural-method-shape"],
|
||||
});
|
||||
|
||||
const matchFields = (
|
||||
left: readonly Keyed<Field>[],
|
||||
right: readonly Keyed<Field>[],
|
||||
maxCandidates: number,
|
||||
) =>
|
||||
matchByKeys({
|
||||
left,
|
||||
right,
|
||||
maxCandidates,
|
||||
exactBasis: "field-signature",
|
||||
fallbackBases: [],
|
||||
});
|
||||
|
||||
interface MatchByKeysInput<Item> {
|
||||
readonly left: readonly Keyed<Item>[];
|
||||
readonly right: readonly Keyed<Item>[];
|
||||
readonly maxCandidates: number;
|
||||
readonly exactBasis: MatchBasis;
|
||||
readonly fallbackBases: readonly MatchBasis[];
|
||||
}
|
||||
|
||||
const matchByKeys = <Item>({
|
||||
left,
|
||||
right,
|
||||
maxCandidates,
|
||||
exactBasis,
|
||||
fallbackBases,
|
||||
}: MatchByKeysInput<Item>): {
|
||||
readonly pairs: readonly MatchedPair<Item>[];
|
||||
readonly ambiguous: readonly Ambiguous<Item>[];
|
||||
readonly leftOnly: readonly Keyed<Item>[];
|
||||
readonly rightOnly: readonly Keyed<Item>[];
|
||||
readonly omittedCandidates: number;
|
||||
} => {
|
||||
const usedLeft = new Set<Keyed<Item>>();
|
||||
const usedRight = new Set<Keyed<Item>>();
|
||||
const pairs: MatchedPair<Item>[] = [];
|
||||
const ambiguous: Ambiguous<Item>[] = [];
|
||||
let omittedCandidates = 0;
|
||||
const rounds: readonly {
|
||||
readonly basis: MatchBasis;
|
||||
readonly key: (item: Keyed<Item>) => string | null;
|
||||
}[] = [
|
||||
{ basis: exactBasis, key: ({ exactKey }) => exactKey },
|
||||
...fallbackBases.map((basis) => ({
|
||||
basis,
|
||||
key: ({ structuralKey }: Keyed<Item>) => structuralKey,
|
||||
})),
|
||||
];
|
||||
for (const round of rounds) {
|
||||
const leftGroups = groupBy(
|
||||
left.filter((item) => !usedLeft.has(item)),
|
||||
round.key,
|
||||
);
|
||||
const rightGroups = groupBy(
|
||||
right.filter((item) => !usedRight.has(item)),
|
||||
round.key,
|
||||
);
|
||||
for (const [key, leftItems] of leftGroups) {
|
||||
const rightItems = rightGroups.get(key);
|
||||
if (rightItems === undefined) continue;
|
||||
if (leftItems.length === 1 && rightItems.length === 1) {
|
||||
const [leftItem] = leftItems;
|
||||
const [rightItem] = rightItems;
|
||||
if (leftItem === undefined || rightItem === undefined) continue;
|
||||
usedLeft.add(leftItem);
|
||||
usedRight.add(rightItem);
|
||||
pairs.push({
|
||||
left: leftItem,
|
||||
right: rightItem,
|
||||
basis: round.basis,
|
||||
confidence:
|
||||
round.basis === "exact-il-signature" ||
|
||||
round.basis === "field-signature"
|
||||
? "exact"
|
||||
: "high",
|
||||
});
|
||||
} else {
|
||||
for (const item of leftItems) usedLeft.add(item);
|
||||
for (const item of rightItems) usedRight.add(item);
|
||||
omittedCandidates += Math.max(
|
||||
0,
|
||||
leftItems.length + rightItems.length - maxCandidates * 2,
|
||||
);
|
||||
ambiguous.push({
|
||||
left: leftItems.slice(0, maxCandidates),
|
||||
right: rightItems.slice(0, maxCandidates),
|
||||
basis: round.basis,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
pairs,
|
||||
ambiguous,
|
||||
leftOnly: left.filter((item) => !usedLeft.has(item)),
|
||||
rightOnly: right.filter((item) => !usedRight.has(item)),
|
||||
omittedCandidates,
|
||||
};
|
||||
};
|
||||
|
||||
const groupBy = <Item>(
|
||||
items: readonly Item[],
|
||||
keyOf: (item: Item) => string | null,
|
||||
): Map<string, readonly Item[]> => {
|
||||
const grouped = new Map<string, Item[]>();
|
||||
for (const item of items) {
|
||||
const key = keyOf(item);
|
||||
if (key === null) continue;
|
||||
const bucket = grouped.get(key) ?? [];
|
||||
bucket.push(item);
|
||||
grouped.set(key, bucket);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
const changedMethodDimensions = (
|
||||
left: Method,
|
||||
right: Method,
|
||||
): MethodItem["dimensions"] => {
|
||||
const dimensions: MethodItem["dimensions"] = [];
|
||||
if (left.signature.raw_sha256 !== right.signature.raw_sha256)
|
||||
dimensions.push("signature");
|
||||
if (left.body.normalized_il_sha256 !== right.body.normalized_il_sha256)
|
||||
dimensions.push("cil");
|
||||
if (
|
||||
canonicalize(left.body.opcode_counts) !==
|
||||
canonicalize(right.body.opcode_counts)
|
||||
)
|
||||
dimensions.push("opcode-shape");
|
||||
if (callShape(left.body.anchors) !== callShape(right.body.anchors))
|
||||
dimensions.push("call-shape");
|
||||
if (fieldShape(left.body.anchors) !== fieldShape(right.body.anchors))
|
||||
dimensions.push("field-shape");
|
||||
if (
|
||||
canonicalize(left.body.exception_regions) !==
|
||||
canonicalize(right.body.exception_regions)
|
||||
)
|
||||
dimensions.push("exception-shape");
|
||||
if (left.body.status !== right.body.status) dimensions.push("availability");
|
||||
return dimensions;
|
||||
};
|
||||
|
||||
const callShape = (anchors: Method["body"]["anchors"]): string =>
|
||||
JSON.stringify(
|
||||
anchors
|
||||
.filter(({ operand_kind }) => operand_kind === "method")
|
||||
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
|
||||
);
|
||||
|
||||
const fieldShape = (anchors: Method["body"]["anchors"]): string =>
|
||||
JSON.stringify(
|
||||
anchors
|
||||
.filter(({ operand_kind }) => operand_kind === "field")
|
||||
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
|
||||
);
|
||||
|
||||
const methodIdentity = (item: Method): NonNullable<MethodItem["left"]> => ({
|
||||
token: item.token,
|
||||
declaring_type: item.declaring_type,
|
||||
name: item.name,
|
||||
signature_sha256: item.signature.raw_sha256,
|
||||
normalized_il_sha256: item.body.normalized_il_sha256,
|
||||
});
|
||||
|
||||
const fieldIdentity = (item: Field): NonNullable<FieldItem["left"]> => ({
|
||||
token: item.token,
|
||||
declaring_type: item.declaring_type,
|
||||
name: item.name,
|
||||
signature_sha256: item.signature.raw_sha256,
|
||||
});
|
||||
|
||||
const methodOnlyItem = (
|
||||
item: Method,
|
||||
context: ComparisonItemContext,
|
||||
left: boolean,
|
||||
): MethodItem => {
|
||||
const absenceObserved = left ? context.rightComplete : context.leftComplete;
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
|
||||
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
|
||||
left: left ? methodIdentity(item) : null,
|
||||
right: left ? null : methodIdentity(item),
|
||||
match: {
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
`unknown-within-unobserved-page: The ${left ? "right" : "left"} method page is incomplete, so absence was not observed.`,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const fieldOnlyItem = (
|
||||
item: Field,
|
||||
context: ComparisonItemContext,
|
||||
left: boolean,
|
||||
): FieldItem => {
|
||||
const absenceObserved = left ? context.rightComplete : context.leftComplete;
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
|
||||
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
|
||||
left: left ? fieldIdentity(item) : null,
|
||||
right: left ? null : fieldIdentity(item),
|
||||
match: {
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: absenceObserved
|
||||
? []
|
||||
: [
|
||||
`unknown-within-unobserved-page: The ${left ? "right" : "left"} field page is incomplete, so absence was not observed.`,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const buildMethodItems = (
|
||||
matches: ReturnType<typeof matchMethods>,
|
||||
context: ComparisonItemContext,
|
||||
): { readonly items: MethodItem[]; readonly omitted: number } => {
|
||||
const items: MethodItem[] = [];
|
||||
for (const pair of matches.pairs) {
|
||||
const dimensions = changedMethodDimensions(pair.left.item, pair.right.item);
|
||||
items.push({
|
||||
item_id: `mmc_method_${sha256({
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
basis: pair.basis,
|
||||
})}`,
|
||||
status: dimensions.length === 0 ? "unchanged" : "changed",
|
||||
left: methodIdentity(pair.left.item),
|
||||
right: methodIdentity(pair.right.item),
|
||||
match: {
|
||||
status: "matched",
|
||||
basis: pair.basis,
|
||||
confidence: pair.confidence,
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
dimensions,
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [],
|
||||
});
|
||||
}
|
||||
for (const ambiguous of matches.ambiguous) {
|
||||
items.push({
|
||||
item_id: `mmc_method_${sha256({
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
basis: ambiguous.basis,
|
||||
})}`,
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: null,
|
||||
match: {
|
||||
status: "ambiguous",
|
||||
basis: ambiguous.basis,
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
|
||||
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
|
||||
},
|
||||
dimensions: ["availability"],
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [
|
||||
"Multiple managed methods share the same non-name identity key; REA did not guess a token remap.",
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(methodOnlyItem(item.item, context, true));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(methodOnlyItem(item.item, context, false));
|
||||
return limitItems(items, context.limits.max_method_matches);
|
||||
};
|
||||
|
||||
export const buildFieldItems = (
|
||||
matches: ReturnType<typeof matchFields>,
|
||||
context: ComparisonItemContext,
|
||||
): { readonly items: FieldItem[]; readonly omitted: number } => {
|
||||
const items: FieldItem[] = [];
|
||||
for (const pair of matches.pairs) {
|
||||
const changed =
|
||||
pair.left.item.signature.raw_sha256 !==
|
||||
pair.right.item.signature.raw_sha256 ||
|
||||
pair.left.item.flags !== pair.right.item.flags;
|
||||
items.push({
|
||||
item_id: `mmc_field_${sha256({
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
})}`,
|
||||
status: changed ? "changed" : "unchanged",
|
||||
left: fieldIdentity(pair.left.item),
|
||||
right: fieldIdentity(pair.right.item),
|
||||
match: {
|
||||
status: "matched",
|
||||
basis: pair.basis,
|
||||
confidence: pair.confidence,
|
||||
candidate_left_tokens: [],
|
||||
candidate_right_tokens: [],
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [],
|
||||
});
|
||||
}
|
||||
for (const ambiguous of matches.ambiguous)
|
||||
items.push({
|
||||
item_id: `mmc_field_${sha256({
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
})}`,
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: null,
|
||||
match: {
|
||||
status: "ambiguous",
|
||||
basis: ambiguous.basis,
|
||||
confidence: "unknown",
|
||||
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
|
||||
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
|
||||
},
|
||||
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
|
||||
limitations: [
|
||||
"Multiple managed fields share the same signature; REA did not guess a token remap from names.",
|
||||
],
|
||||
});
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(fieldOnlyItem(item.item, context, true));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(fieldOnlyItem(item.item, context, false));
|
||||
return limitItems(items, context.limits.max_field_matches);
|
||||
};
|
||||
|
||||
const limitItems = <Item>(
|
||||
items: readonly Item[],
|
||||
limit: number,
|
||||
): { readonly items: Item[]; readonly omitted: number } => ({
|
||||
items: items.slice(0, limit),
|
||||
omitted: Math.max(0, items.length - limit),
|
||||
});
|
||||
|
||||
export const keyMembers = (
|
||||
left: ManagedMemberComparisonSide,
|
||||
right: ManagedMemberComparisonSide,
|
||||
maxCandidates: number,
|
||||
): {
|
||||
readonly methodMatches: ReturnType<typeof matchMethods>;
|
||||
readonly fieldMatches: ReturnType<typeof matchFields>;
|
||||
} => ({
|
||||
methodMatches: matchMethods(
|
||||
left.result.methods.items.map(keyMethod),
|
||||
right.result.methods.items.map(keyMethod),
|
||||
maxCandidates,
|
||||
),
|
||||
fieldMatches: matchFields(
|
||||
left.result.fields.items.map(keyField),
|
||||
right.result.fields.items.map(keyField),
|
||||
maxCandidates,
|
||||
),
|
||||
});
|
||||
|
||||
export const parseManagedMemberEvidence = (
|
||||
evidence: unknown,
|
||||
): {
|
||||
readonly evidenceId: string;
|
||||
readonly result: ManagedMemberInspection;
|
||||
} => {
|
||||
const parsed = parseEvidence(evidence);
|
||||
if (parsed.operation !== "inspect_managed_members")
|
||||
throw new TypeError("Evidence operation is not inspect_managed_members");
|
||||
return {
|
||||
evidenceId: parsed.evidence_id,
|
||||
result: managedMemberInspectionSchema.parse(parsed.normalized_result),
|
||||
};
|
||||
};
|
||||
@@ -1,181 +1,30 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, parseEvidence } from "./evidence.js";
|
||||
import { functionDossierSchema } from "./hopperValues.js";
|
||||
import { parseEvidence } from "./evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import { inspectMachoSchema } from "./nativeInspection.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import {
|
||||
managedNativeVerificationResultSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
type ManagedNativeVerificationResult,
|
||||
} from "./managedNativeVerificationSchemas.js";
|
||||
import {
|
||||
buildVerificationResult,
|
||||
collectNativeSymbols,
|
||||
verifyPinvoke,
|
||||
} from "./managedNativeVerificationMatch.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const boundedTextSchema = z.string().min(1).max(4_096);
|
||||
|
||||
const nativeVerificationLimitsSchema = z.strictObject({
|
||||
max_native_observations: z.number().int().min(1).max(50).default(20),
|
||||
max_candidates_per_import: z.number().int().min(1).max(100).default(25),
|
||||
});
|
||||
|
||||
/** Authenticated managed boundary Evidence plus native observations. */
|
||||
export const managedNativeVerificationInputSchema = z
|
||||
.strictObject({
|
||||
managed_boundaries: evidenceSchema,
|
||||
native_observations: z.array(evidenceSchema).min(1).max(50),
|
||||
limits: nativeVerificationLimitsSchema.default({
|
||||
max_native_observations: 20,
|
||||
max_candidates_per_import: 25,
|
||||
}),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
const ids = new Set<string>();
|
||||
for (const [index, evidence] of input.native_observations.entries()) {
|
||||
if (evidence.evidence_id === input.managed_boundaries.evidence_id) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["native_observations", index],
|
||||
message:
|
||||
"Native observation Evidence must be distinct from managed boundary Evidence",
|
||||
});
|
||||
}
|
||||
if (ids.has(evidence.evidence_id)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["native_observations", index],
|
||||
message: "Native observation Evidence IDs must be unique",
|
||||
});
|
||||
}
|
||||
ids.add(evidence.evidence_id);
|
||||
}
|
||||
});
|
||||
|
||||
const nativeSymbolSchema = z.strictObject({
|
||||
evidence_id: evidenceIdSchema,
|
||||
operation: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
address: z.string().nullable(),
|
||||
module_name: z.string().nullable(),
|
||||
module_path: z.string().nullable(),
|
||||
source: z.enum(["macho-export", "function-dossier"]),
|
||||
});
|
||||
|
||||
const matchStatusSchema = z.enum([
|
||||
"verified",
|
||||
"inferred",
|
||||
"unresolved",
|
||||
"contradicted",
|
||||
]);
|
||||
|
||||
const matchBasisSchema = z.enum([
|
||||
"exact-export-name",
|
||||
"exact-function-name",
|
||||
"decorated-name-candidate",
|
||||
"module-mismatch",
|
||||
"no-native-candidate",
|
||||
"unsupported-native-evidence",
|
||||
]);
|
||||
|
||||
const pinvokeVerificationSchema = z.strictObject({
|
||||
item_id: z.string().regex(/^mnv_pinvoke_[a-f0-9]{64}$/u),
|
||||
managed: z.strictObject({
|
||||
token: tokenSchema,
|
||||
member_token: tokenSchema.nullable(),
|
||||
member_name: z.string().nullable(),
|
||||
import_name: z.string().min(1),
|
||||
import_scope_name: z.string().nullable(),
|
||||
no_mangle: z.boolean(),
|
||||
char_set: z.enum(["not-specified", "ansi", "unicode", "auto", "unknown"]),
|
||||
call_convention: z.enum([
|
||||
"not-specified",
|
||||
"winapi",
|
||||
"cdecl",
|
||||
"stdcall",
|
||||
"thiscall",
|
||||
"fastcall",
|
||||
"unknown",
|
||||
]),
|
||||
declaration_verification: z.literal("managed-declaration-only"),
|
||||
}),
|
||||
status: matchStatusSchema,
|
||||
basis: matchBasisSchema,
|
||||
confidence: z.enum(["observed", "inferred", "unknown"]),
|
||||
matched_native: nativeSymbolSchema.nullable(),
|
||||
candidates: z.array(nativeSymbolSchema).max(100),
|
||||
evidence_links: z.array(evidenceIdSchema).min(1).max(51),
|
||||
limitations: z.array(boundedTextSchema).max(100),
|
||||
});
|
||||
|
||||
/** Provider-neutral managed/native verification result. */
|
||||
export const managedNativeVerificationResultSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
verification_id: z.string().regex(/^mnv_[a-f0-9]{64}$/u),
|
||||
algorithm: z.strictObject({
|
||||
name: z.literal("rea-managed-native-verification"),
|
||||
version: z.literal(1),
|
||||
token_identity: z.literal("build-local"),
|
||||
token_to_address_mapping: z.literal("not-inferred"),
|
||||
}),
|
||||
managed_boundary: z.strictObject({
|
||||
evidence_id: evidenceIdSchema,
|
||||
artifact_sha256: digestSchema,
|
||||
artifact_path: z.string().min(1),
|
||||
mvid: cliMetadataGuidSchema.nullable(),
|
||||
metadata_status: z.enum(["absent", "complete", "partial", "malformed"]),
|
||||
pinvoke_imports_total: z.number().int().min(0),
|
||||
native_implementations_total: z.number().int().min(0),
|
||||
coverage_state: z.enum(["complete", "partial", "unavailable"]),
|
||||
}),
|
||||
native_observations: z.strictObject({
|
||||
total: z.number().int().min(1),
|
||||
accepted: z.number().int().min(0),
|
||||
unsupported: z.number().int().min(0),
|
||||
symbols: z.number().int().min(0),
|
||||
truncated: z.boolean(),
|
||||
}),
|
||||
summary: z.strictObject({
|
||||
verified: z.number().int().min(0),
|
||||
inferred: z.number().int().min(0),
|
||||
unresolved: z.number().int().min(0),
|
||||
contradicted: z.number().int().min(0),
|
||||
native_body_unresolved: z.number().int().min(0),
|
||||
}),
|
||||
pinvoke_imports: z.array(pinvokeVerificationSchema).max(50_000),
|
||||
native_implementations: z.strictObject({
|
||||
unresolved: z.number().int().min(0),
|
||||
reason: boundedTextSchema,
|
||||
}),
|
||||
coverage: z.strictObject({
|
||||
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
|
||||
omitted_native_observations: z.number().int().min(0),
|
||||
omitted_candidates: z.number().int().min(0),
|
||||
}),
|
||||
evidence_links: z.array(evidenceIdSchema).min(2).max(51),
|
||||
limitations: z.array(boundedTextSchema).max(1_000),
|
||||
});
|
||||
|
||||
export type ManagedNativeVerificationInput = z.infer<
|
||||
typeof managedNativeVerificationInputSchema
|
||||
>;
|
||||
export type ManagedNativeVerificationResult = z.infer<
|
||||
typeof managedNativeVerificationResultSchema
|
||||
>;
|
||||
|
||||
type NativeSymbol = z.infer<typeof nativeSymbolSchema>;
|
||||
type PinvokeImport =
|
||||
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
|
||||
type PinvokeVerification = z.infer<typeof pinvokeVerificationSchema>;
|
||||
type VerifiedPinvoke = {
|
||||
readonly verification: PinvokeVerification;
|
||||
readonly omittedCandidates: number;
|
||||
};
|
||||
export {
|
||||
managedNativeVerificationInputSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
type ManagedNativeVerificationResult,
|
||||
} from "./managedNativeVerificationSchemas.js";
|
||||
export { managedNativeVerificationResultSchema };
|
||||
|
||||
const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
@@ -184,346 +33,50 @@ const sha256 = (value: JsonValue): string => {
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
/** Verify managed P/Invoke declarations against authenticated native Evidence. */
|
||||
export const verifyManagedNativeBoundaries = (
|
||||
const managedBoundaryFromInput = (
|
||||
input: ManagedNativeVerificationInput,
|
||||
): ManagedNativeVerificationResult => {
|
||||
): ManagedNativeBoundaryInspection => {
|
||||
const managedEvidence = parseEvidence(input.managed_boundaries);
|
||||
if (managedEvidence.operation !== "inspect_managed_native_boundaries")
|
||||
throw new TypeError(
|
||||
"Evidence operation is not inspect_managed_native_boundaries",
|
||||
);
|
||||
const managed = managedNativeBoundaryInspectionSchema.parse(
|
||||
return managedNativeBoundaryInspectionSchema.parse(
|
||||
managedEvidence.normalized_result,
|
||||
);
|
||||
};
|
||||
|
||||
/** Verify managed P/Invoke declarations against authenticated native Evidence. */
|
||||
export const verifyManagedNativeBoundaries = (
|
||||
input: ManagedNativeVerificationInput,
|
||||
): ManagedNativeVerificationResult => {
|
||||
const managed = managedBoundaryFromInput(input);
|
||||
const managedEvidence = parseEvidence(input.managed_boundaries);
|
||||
const native = collectNativeSymbols(
|
||||
input.native_observations.slice(0, input.limits.max_native_observations),
|
||||
);
|
||||
const verifiedPinvokes = managed.pinvoke_imports.items.map((item) =>
|
||||
verifyPinvoke(
|
||||
verifyPinvoke({
|
||||
item,
|
||||
managedEvidence.evidence_id,
|
||||
native.symbols,
|
||||
native.accepted > 0,
|
||||
managedEvidenceId: managedEvidence.evidence_id,
|
||||
symbols: native.symbols,
|
||||
hasSupportedNativeEvidence: native.accepted > 0,
|
||||
input,
|
||||
),
|
||||
}),
|
||||
);
|
||||
const pinvokeImports = verifiedPinvokes.map(
|
||||
({ verification }) => verification,
|
||||
);
|
||||
const counts = countStatuses(pinvokeImports);
|
||||
const omittedNativeObservations = Math.max(
|
||||
0,
|
||||
input.native_observations.length - input.limits.max_native_observations,
|
||||
);
|
||||
const omittedCandidates = verifiedPinvokes.reduce(
|
||||
(sum, item) => sum + item.omittedCandidates,
|
||||
0,
|
||||
);
|
||||
const coverageStatus =
|
||||
omittedNativeObservations > 0 || omittedCandidates > 0
|
||||
? "truncated"
|
||||
: managed.coverage.state === "complete" && native.unsupported === 0
|
||||
? "complete-within-inputs"
|
||||
: "partial";
|
||||
const withoutId = {
|
||||
schema_version: 1 as const,
|
||||
algorithm: {
|
||||
name: "rea-managed-native-verification" as const,
|
||||
version: 1 as const,
|
||||
token_identity: "build-local" as const,
|
||||
token_to_address_mapping: "not-inferred" as const,
|
||||
},
|
||||
managed_boundary: {
|
||||
evidence_id: managedEvidence.evidence_id,
|
||||
artifact_sha256: managed.artifact.sha256,
|
||||
artifact_path: managed.artifact.path,
|
||||
mvid: managed.module?.mvid ?? null,
|
||||
metadata_status: managed.metadata.status,
|
||||
pinvoke_imports_total: managed.pinvoke_imports.total,
|
||||
native_implementations_total: managed.native_implementations.total,
|
||||
coverage_state: managed.coverage.state,
|
||||
},
|
||||
native_observations: {
|
||||
total: input.native_observations.length,
|
||||
accepted: native.accepted,
|
||||
unsupported: native.unsupported,
|
||||
symbols: native.symbols.length,
|
||||
truncated: omittedNativeObservations > 0,
|
||||
},
|
||||
summary: {
|
||||
...counts,
|
||||
native_body_unresolved: managed.native_implementations.items.filter(
|
||||
({ boundary_kind: kind }) => kind !== "pinvoke",
|
||||
).length,
|
||||
},
|
||||
pinvoke_imports: pinvokeImports,
|
||||
native_implementations: {
|
||||
unresolved: managed.native_implementations.items.filter(
|
||||
({ boundary_kind: kind }) => kind !== "pinvoke",
|
||||
).length,
|
||||
reason:
|
||||
"Managed metadata tokens and RVAs are not translated to native provider addresses by this workflow; C++/CLI, ReadyToRun, and native-body mappings require explicit provider-supported bridge evidence.",
|
||||
},
|
||||
coverage: {
|
||||
status: coverageStatus,
|
||||
omitted_native_observations: omittedNativeObservations,
|
||||
omitted_candidates: omittedCandidates,
|
||||
},
|
||||
evidence_links: [
|
||||
managedEvidence.evidence_id,
|
||||
...input.native_observations
|
||||
.slice(0, input.limits.max_native_observations)
|
||||
.map(({ evidence_id: id }) => id),
|
||||
],
|
||||
limitations: [
|
||||
"P/Invoke verification checks declared import names against supplied native export or function-name Evidence only.",
|
||||
"A matching native symbol verifies that a candidate export/function was observed; it does not prove CLR binding, marshaling behavior, call reachability, or runtime loading.",
|
||||
"Missing supplied native symbols are reported as unresolved, not proof that a dependency cannot exist.",
|
||||
"Managed metadata tokens and method RVAs are not interpreted as native addresses.",
|
||||
...(managed.coverage.state === "complete"
|
||||
? []
|
||||
: ["Managed boundary input is partial or unavailable."]),
|
||||
...(native.unsupported === 0
|
||||
? []
|
||||
: [
|
||||
"Some native Evidence operations were unsupported by this workflow.",
|
||||
]),
|
||||
],
|
||||
};
|
||||
const withoutId = buildVerificationResult({
|
||||
managedEvidence,
|
||||
managed,
|
||||
native,
|
||||
pinvokeImports,
|
||||
verifiedPinvokes,
|
||||
input,
|
||||
});
|
||||
return managedNativeVerificationResultSchema.parse({
|
||||
...withoutId,
|
||||
verification_id: `mnv_${sha256(withoutId)}`,
|
||||
});
|
||||
};
|
||||
|
||||
const collectNativeSymbols = (
|
||||
observations: readonly ManagedNativeVerificationInput["native_observations"][number][],
|
||||
): {
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
readonly accepted: number;
|
||||
readonly unsupported: number;
|
||||
} => {
|
||||
const symbols: NativeSymbol[] = [];
|
||||
let accepted = 0;
|
||||
let unsupported = 0;
|
||||
for (const raw of observations) {
|
||||
const evidence = parseEvidence(raw);
|
||||
const extracted = symbolsForEvidence(evidence);
|
||||
if (!extracted.supported) {
|
||||
unsupported += 1;
|
||||
} else {
|
||||
accepted += 1;
|
||||
symbols.push(...extracted.symbols);
|
||||
}
|
||||
}
|
||||
return { symbols, accepted, unsupported };
|
||||
};
|
||||
|
||||
const symbolsForEvidence = (
|
||||
evidence: ReturnType<typeof parseEvidence>,
|
||||
): {
|
||||
readonly supported: boolean;
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
} => {
|
||||
if (evidence.operation === "inspect_macho") {
|
||||
const result = inspectMachoSchema.parse(evidence.normalized_result);
|
||||
return {
|
||||
supported: true,
|
||||
symbols: result.exports.items.map((symbol) => ({
|
||||
evidence_id: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
name: symbol.name,
|
||||
address: symbol.address,
|
||||
module_name: moduleName(evidence),
|
||||
module_path: modulePath(evidence),
|
||||
source: "macho-export" as const,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (evidence.operation === "analyze_function") {
|
||||
const dossier = functionDossierSchema.parse(evidence.normalized_result);
|
||||
return {
|
||||
supported: true,
|
||||
symbols: [
|
||||
{
|
||||
evidence_id: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
name: dossier.procedure.name,
|
||||
address: dossier.procedure.address,
|
||||
module_name: moduleName(evidence),
|
||||
module_path: modulePath(evidence),
|
||||
source: "function-dossier" as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { supported: false, symbols: [] };
|
||||
};
|
||||
|
||||
const verifyPinvoke = (
|
||||
item: PinvokeImport,
|
||||
managedEvidenceId: string,
|
||||
symbols: readonly NativeSymbol[],
|
||||
hasSupportedNativeEvidence: boolean,
|
||||
input: ManagedNativeVerificationInput,
|
||||
): VerifiedPinvoke => {
|
||||
const managed = {
|
||||
token: item.token,
|
||||
member_token: item.member_token,
|
||||
member_name: item.member_name,
|
||||
import_name: item.import_name,
|
||||
import_scope_name: item.import_scope_name,
|
||||
no_mangle: item.no_mangle,
|
||||
char_set: item.char_set,
|
||||
call_convention: item.call_convention,
|
||||
declaration_verification: item.verification,
|
||||
};
|
||||
const names = candidateNames(managed);
|
||||
const allCandidates = symbols.filter((symbol) =>
|
||||
names.some((name) => sameSymbolName(name, symbol.name)),
|
||||
);
|
||||
const candidates = allCandidates.slice(
|
||||
0,
|
||||
input.limits.max_candidates_per_import,
|
||||
);
|
||||
const exact = candidates.find(
|
||||
(symbol) =>
|
||||
sameSymbolName(item.import_name, symbol.name) &&
|
||||
moduleCompatible(item.import_scope_name, symbol),
|
||||
);
|
||||
const decorated = candidates.find((symbol) =>
|
||||
moduleCompatible(item.import_scope_name, symbol),
|
||||
);
|
||||
const selected = exact ?? decorated ?? candidates[0] ?? null;
|
||||
const basis =
|
||||
selected === null
|
||||
? hasSupportedNativeEvidence
|
||||
? "no-native-candidate"
|
||||
: "unsupported-native-evidence"
|
||||
: !moduleCompatible(item.import_scope_name, selected)
|
||||
? "module-mismatch"
|
||||
: exact !== undefined
|
||||
? selected.source === "macho-export"
|
||||
? "exact-export-name"
|
||||
: "exact-function-name"
|
||||
: "decorated-name-candidate";
|
||||
const status =
|
||||
selected === null
|
||||
? "unresolved"
|
||||
: basis === "module-mismatch"
|
||||
? "contradicted"
|
||||
: basis === "decorated-name-candidate"
|
||||
? "inferred"
|
||||
: "verified";
|
||||
const limitations = [
|
||||
...(item.import_scope_name === null
|
||||
? ["Managed declaration does not name an import scope/module."]
|
||||
: []),
|
||||
...(status === "inferred"
|
||||
? [
|
||||
"Native symbol matched a decorated candidate rather than the exact declared import name.",
|
||||
]
|
||||
: []),
|
||||
...(status === "contradicted"
|
||||
? [
|
||||
"A symbol name candidate exists, but the supplied native Evidence module identity does not match the managed import scope.",
|
||||
]
|
||||
: []),
|
||||
...(status === "unresolved"
|
||||
? [
|
||||
"No supplied native export/function Evidence matched this declared import.",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return {
|
||||
verification: pinvokeVerificationSchema.parse({
|
||||
item_id: `mnv_pinvoke_${sha256({
|
||||
managedEvidenceId,
|
||||
token: item.token,
|
||||
importName: item.import_name,
|
||||
importScope: item.import_scope_name,
|
||||
candidates,
|
||||
})}`,
|
||||
managed,
|
||||
status,
|
||||
basis,
|
||||
confidence:
|
||||
status === "verified"
|
||||
? "observed"
|
||||
: status === "inferred"
|
||||
? "inferred"
|
||||
: "unknown",
|
||||
matched_native: selected,
|
||||
candidates,
|
||||
evidence_links: [
|
||||
managedEvidenceId,
|
||||
...new Set(candidates.map(({ evidence_id: id }) => id)),
|
||||
],
|
||||
limitations,
|
||||
}),
|
||||
omittedCandidates: Math.max(0, allCandidates.length - candidates.length),
|
||||
};
|
||||
};
|
||||
|
||||
const candidateNames = (
|
||||
managed: Pick<
|
||||
PinvokeImport,
|
||||
"import_name" | "char_set" | "call_convention" | "no_mangle"
|
||||
>,
|
||||
): readonly string[] => {
|
||||
const names = new Set<string>([managed.import_name]);
|
||||
if (!managed.no_mangle) {
|
||||
if (managed.char_set === "unicode") names.add(`${managed.import_name}W`);
|
||||
if (managed.char_set === "ansi") names.add(`${managed.import_name}A`);
|
||||
if (managed.call_convention === "stdcall") {
|
||||
names.add(`_${managed.import_name}`);
|
||||
names.add(`_${managed.import_name}@0`);
|
||||
}
|
||||
}
|
||||
return [...names];
|
||||
};
|
||||
|
||||
const sameSymbolName = (left: string, right: string): boolean =>
|
||||
normalizeSymbol(left) === normalizeSymbol(right);
|
||||
|
||||
const normalizeSymbol = (value: string): string =>
|
||||
value.replace(/^_+/u, "").replace(/@\d+$/u, "").toLowerCase();
|
||||
|
||||
const moduleCompatible = (
|
||||
scope: string | null,
|
||||
symbol: NativeSymbol,
|
||||
): boolean => {
|
||||
if (scope === null) return true;
|
||||
const expected = normalizeModule(scope);
|
||||
return [symbol.module_name, symbol.module_path]
|
||||
.filter((value): value is string => value !== null)
|
||||
.map(normalizeModule)
|
||||
.some((value) => value === expected);
|
||||
};
|
||||
|
||||
const normalizeModule = (value: string): string =>
|
||||
basename(value)
|
||||
.toLowerCase()
|
||||
.replace(/^lib/u, "")
|
||||
.replace(/\.(dll|dylib|so|node|exe)$/u, "");
|
||||
|
||||
const moduleName = (
|
||||
evidence: ReturnType<typeof parseEvidence>,
|
||||
): string | null => evidence.subject?.name ?? null;
|
||||
|
||||
const modulePath = (
|
||||
evidence: ReturnType<typeof parseEvidence>,
|
||||
): string | null => evidence.subject?.local_path ?? null;
|
||||
|
||||
const countStatuses = (
|
||||
items: readonly z.infer<typeof pinvokeVerificationSchema>[],
|
||||
): Pick<
|
||||
ManagedNativeVerificationResult["summary"],
|
||||
"verified" | "inferred" | "unresolved" | "contradicted"
|
||||
> => ({
|
||||
verified: items.filter(({ status }) => status === "verified").length,
|
||||
inferred: items.filter(({ status }) => status === "inferred").length,
|
||||
unresolved: items.filter(({ status }) => status === "unresolved").length,
|
||||
contradicted: items.filter(({ status }) => status === "contradicted").length,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { parseEvidence } from "./evidence.js";
|
||||
import { functionDossierSchema } from "./hopperValues.js";
|
||||
import { inspectMachoSchema } from "./nativeInspection.js";
|
||||
import type { ManagedNativeBoundaryInspection } from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import {
|
||||
pinvokeVerificationSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
type ManagedNativeVerificationResult,
|
||||
type NativeSymbol,
|
||||
type PinvokeVerification,
|
||||
} from "./managedNativeVerificationSchemas.js";
|
||||
|
||||
const digest = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed/native verification canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
type PinvokeImport =
|
||||
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
|
||||
type VerifiedPinvoke = {
|
||||
readonly verification: PinvokeVerification;
|
||||
readonly omittedCandidates: number;
|
||||
};
|
||||
|
||||
const moduleName = (
|
||||
evidence: ReturnType<typeof parseEvidence>,
|
||||
): string | null => evidence.subject?.name ?? null;
|
||||
|
||||
const modulePath = (
|
||||
evidence: ReturnType<typeof parseEvidence>,
|
||||
): string | null => evidence.subject?.local_path ?? null;
|
||||
|
||||
export const symbolsForEvidence = (
|
||||
evidence: ReturnType<typeof parseEvidence>,
|
||||
): {
|
||||
readonly supported: boolean;
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
} => {
|
||||
if (evidence.operation === "inspect_macho") {
|
||||
const result = inspectMachoSchema.parse(evidence.normalized_result);
|
||||
return {
|
||||
supported: true,
|
||||
symbols: result.exports.items.map((symbol) => ({
|
||||
evidence_id: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
name: symbol.name,
|
||||
address: symbol.address,
|
||||
module_name: moduleName(evidence),
|
||||
module_path: modulePath(evidence),
|
||||
source: "macho-export" as const,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (evidence.operation === "analyze_function") {
|
||||
const dossier = functionDossierSchema.parse(evidence.normalized_result);
|
||||
return {
|
||||
supported: true,
|
||||
symbols: [
|
||||
{
|
||||
evidence_id: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
name: dossier.procedure.name,
|
||||
address: dossier.procedure.address,
|
||||
module_name: moduleName(evidence),
|
||||
module_path: modulePath(evidence),
|
||||
source: "function-dossier" as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { supported: false, symbols: [] };
|
||||
};
|
||||
|
||||
export const collectNativeSymbols = (
|
||||
observations: readonly ManagedNativeVerificationInput["native_observations"][number][],
|
||||
): {
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
readonly accepted: number;
|
||||
readonly unsupported: number;
|
||||
} => {
|
||||
const symbols: NativeSymbol[] = [];
|
||||
let accepted = 0;
|
||||
let unsupported = 0;
|
||||
for (const raw of observations) {
|
||||
const evidence = parseEvidence(raw);
|
||||
const extracted = symbolsForEvidence(evidence);
|
||||
if (!extracted.supported) {
|
||||
unsupported += 1;
|
||||
} else {
|
||||
accepted += 1;
|
||||
symbols.push(...extracted.symbols);
|
||||
}
|
||||
}
|
||||
return { symbols, accepted, unsupported };
|
||||
};
|
||||
|
||||
const normalizeSymbol = (value: string): string =>
|
||||
value.replace(/^_+/u, "").replace(/@\d+$/u, "").toLowerCase();
|
||||
|
||||
const sameSymbolName = (left: string, right: string): boolean =>
|
||||
normalizeSymbol(left) === normalizeSymbol(right);
|
||||
|
||||
const normalizeModule = (value: string): string =>
|
||||
basename(value)
|
||||
.toLowerCase()
|
||||
.replace(/^lib/u, "")
|
||||
.replace(/\.(dll|dylib|so|node|exe)$/u, "");
|
||||
|
||||
const moduleCompatible = (
|
||||
scope: string | null,
|
||||
symbol: NativeSymbol,
|
||||
): boolean => {
|
||||
if (scope === null) return true;
|
||||
const expected = normalizeModule(scope);
|
||||
return [symbol.module_name, symbol.module_path]
|
||||
.filter((value): value is string => value !== null)
|
||||
.map(normalizeModule)
|
||||
.some((value) => value === expected);
|
||||
};
|
||||
|
||||
const candidateNames = (
|
||||
managed: Pick<
|
||||
PinvokeImport,
|
||||
"import_name" | "char_set" | "call_convention" | "no_mangle"
|
||||
>,
|
||||
): readonly string[] => {
|
||||
const names = new Set<string>([managed.import_name]);
|
||||
if (!managed.no_mangle) {
|
||||
if (managed.char_set === "unicode") names.add(`${managed.import_name}W`);
|
||||
if (managed.char_set === "ansi") names.add(`${managed.import_name}A`);
|
||||
if (managed.call_convention === "stdcall") {
|
||||
names.add(`_${managed.import_name}`);
|
||||
names.add(`_${managed.import_name}@0`);
|
||||
}
|
||||
}
|
||||
return [...names];
|
||||
};
|
||||
|
||||
interface CandidateSelection {
|
||||
readonly allCandidates: readonly NativeSymbol[];
|
||||
readonly candidates: readonly NativeSymbol[];
|
||||
readonly selected: NativeSymbol | null;
|
||||
readonly exact: NativeSymbol | undefined;
|
||||
readonly decorated: NativeSymbol | undefined;
|
||||
}
|
||||
|
||||
const selectCandidates = (
|
||||
managed: PinvokeImport,
|
||||
symbols: readonly NativeSymbol[],
|
||||
maxCandidates: number,
|
||||
): CandidateSelection => {
|
||||
const names = candidateNames(managed);
|
||||
const allCandidates = symbols.filter((symbol) =>
|
||||
names.some((name) => sameSymbolName(name, symbol.name)),
|
||||
);
|
||||
const candidates = allCandidates.slice(0, maxCandidates);
|
||||
const exact = candidates.find(
|
||||
(symbol) =>
|
||||
sameSymbolName(managed.import_name, symbol.name) &&
|
||||
moduleCompatible(managed.import_scope_name, symbol),
|
||||
);
|
||||
const decorated = candidates.find((symbol) =>
|
||||
moduleCompatible(managed.import_scope_name, symbol),
|
||||
);
|
||||
const selected = exact ?? decorated ?? candidates[0] ?? null;
|
||||
return { allCandidates, candidates, selected, exact, decorated };
|
||||
};
|
||||
|
||||
interface BasisAndStatus {
|
||||
readonly basis: PinvokeVerification["basis"];
|
||||
readonly status: PinvokeVerification["status"];
|
||||
readonly confidence: PinvokeVerification["confidence"];
|
||||
}
|
||||
|
||||
const basisAndStatus = (
|
||||
managed: PinvokeImport,
|
||||
selection: CandidateSelection,
|
||||
hasSupportedNativeEvidence: boolean,
|
||||
): BasisAndStatus => {
|
||||
const { selected, exact } = selection;
|
||||
if (selected === null)
|
||||
return {
|
||||
basis: hasSupportedNativeEvidence
|
||||
? "no-native-candidate"
|
||||
: "unsupported-native-evidence",
|
||||
status: "unresolved",
|
||||
confidence: "unknown",
|
||||
};
|
||||
if (!moduleCompatible(managed.import_scope_name, selected))
|
||||
return {
|
||||
basis: "module-mismatch",
|
||||
status: "contradicted",
|
||||
confidence: "unknown",
|
||||
};
|
||||
if (exact !== undefined)
|
||||
return {
|
||||
basis:
|
||||
selected.source === "macho-export"
|
||||
? "exact-export-name"
|
||||
: "exact-function-name",
|
||||
status: "verified",
|
||||
confidence: "observed",
|
||||
};
|
||||
return {
|
||||
basis: "decorated-name-candidate",
|
||||
status: "inferred",
|
||||
confidence: "inferred",
|
||||
};
|
||||
};
|
||||
|
||||
const pinvokeLimitations = (
|
||||
status: PinvokeVerification["status"],
|
||||
managed: PinvokeImport,
|
||||
): readonly string[] => [
|
||||
...(managed.import_scope_name === null
|
||||
? ["Managed declaration does not name an import scope/module."]
|
||||
: []),
|
||||
...(status === "inferred"
|
||||
? [
|
||||
"Native symbol matched a decorated candidate rather than the exact declared import name.",
|
||||
]
|
||||
: []),
|
||||
...(status === "contradicted"
|
||||
? [
|
||||
"A symbol name candidate exists, but the supplied native Evidence module identity does not match the managed import scope.",
|
||||
]
|
||||
: []),
|
||||
...(status === "unresolved"
|
||||
? [
|
||||
"No supplied native export/function Evidence matched this declared import.",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
interface VerifyPinvokeContext {
|
||||
readonly item: PinvokeImport;
|
||||
readonly managedEvidenceId: string;
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
readonly hasSupportedNativeEvidence: boolean;
|
||||
readonly input: ManagedNativeVerificationInput;
|
||||
}
|
||||
|
||||
export const verifyPinvoke = ({
|
||||
item,
|
||||
managedEvidenceId,
|
||||
symbols,
|
||||
hasSupportedNativeEvidence,
|
||||
input,
|
||||
}: VerifyPinvokeContext): VerifiedPinvoke => {
|
||||
const managed = {
|
||||
token: item.token,
|
||||
member_token: item.member_token,
|
||||
member_name: item.member_name,
|
||||
import_name: item.import_name,
|
||||
import_scope_name: item.import_scope_name,
|
||||
no_mangle: item.no_mangle,
|
||||
char_set: item.char_set,
|
||||
call_convention: item.call_convention,
|
||||
declaration_verification: item.verification,
|
||||
};
|
||||
const selection = selectCandidates(
|
||||
item,
|
||||
symbols,
|
||||
input.limits.max_candidates_per_import,
|
||||
);
|
||||
const { status, basis, confidence } = basisAndStatus(
|
||||
item,
|
||||
selection,
|
||||
hasSupportedNativeEvidence,
|
||||
);
|
||||
const limitations = pinvokeLimitations(status, item);
|
||||
return {
|
||||
verification: pinvokeVerificationSchema.parse({
|
||||
item_id: `mnv_pinvoke_${digest({
|
||||
managedEvidenceId,
|
||||
token: item.token,
|
||||
importName: item.import_name,
|
||||
importScope: item.import_scope_name,
|
||||
candidates: [...selection.candidates],
|
||||
})}`,
|
||||
managed,
|
||||
status,
|
||||
basis,
|
||||
confidence,
|
||||
matched_native: selection.selected,
|
||||
candidates: selection.candidates,
|
||||
evidence_links: [
|
||||
managedEvidenceId,
|
||||
...new Set(selection.candidates.map(({ evidence_id: id }) => id)),
|
||||
],
|
||||
limitations,
|
||||
}),
|
||||
omittedCandidates: Math.max(
|
||||
0,
|
||||
selection.allCandidates.length - selection.candidates.length,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const countStatuses = (
|
||||
items: readonly PinvokeVerification[],
|
||||
): Pick<
|
||||
ManagedNativeVerificationResult["summary"],
|
||||
"verified" | "inferred" | "unresolved" | "contradicted"
|
||||
> => ({
|
||||
verified: items.filter(({ status }) => status === "verified").length,
|
||||
inferred: items.filter(({ status }) => status === "inferred").length,
|
||||
unresolved: items.filter(({ status }) => status === "unresolved").length,
|
||||
contradicted: items.filter(({ status }) => status === "contradicted").length,
|
||||
});
|
||||
|
||||
interface VerificationResultInput {
|
||||
readonly managedEvidence: ReturnType<typeof parseEvidence>;
|
||||
readonly managed: ManagedNativeBoundaryInspection;
|
||||
readonly native: {
|
||||
readonly symbols: readonly NativeSymbol[];
|
||||
readonly accepted: number;
|
||||
readonly unsupported: number;
|
||||
};
|
||||
readonly pinvokeImports: readonly PinvokeVerification[];
|
||||
readonly verifiedPinvokes: readonly VerifiedPinvoke[];
|
||||
readonly input: ManagedNativeVerificationInput;
|
||||
}
|
||||
|
||||
export const buildVerificationResult = ({
|
||||
managedEvidence,
|
||||
managed,
|
||||
native,
|
||||
pinvokeImports,
|
||||
verifiedPinvokes,
|
||||
input,
|
||||
}: VerificationResultInput): Omit<
|
||||
ManagedNativeVerificationResult,
|
||||
"verification_id"
|
||||
> => {
|
||||
const counts = countStatuses(pinvokeImports);
|
||||
const omittedNativeObservations = Math.max(
|
||||
0,
|
||||
input.native_observations.length - input.limits.max_native_observations,
|
||||
);
|
||||
const omittedCandidates = verifiedPinvokes.reduce(
|
||||
(sum, item) => sum + item.omittedCandidates,
|
||||
0,
|
||||
);
|
||||
const nativeBodyUnresolved = managed.native_implementations.items.filter(
|
||||
({ boundary_kind: kind }) => kind !== "pinvoke",
|
||||
).length;
|
||||
const coverageStatus =
|
||||
omittedNativeObservations > 0 || omittedCandidates > 0
|
||||
? "truncated"
|
||||
: managed.coverage.state === "complete" && native.unsupported === 0
|
||||
? "complete-within-inputs"
|
||||
: "partial";
|
||||
return {
|
||||
schema_version: 1 as const,
|
||||
algorithm: {
|
||||
name: "rea-managed-native-verification" as const,
|
||||
version: 1 as const,
|
||||
token_identity: "build-local" as const,
|
||||
token_to_address_mapping: "not-inferred" as const,
|
||||
},
|
||||
managed_boundary: {
|
||||
evidence_id: managedEvidence.evidence_id,
|
||||
artifact_sha256: managed.artifact.sha256,
|
||||
artifact_path: managed.artifact.path,
|
||||
mvid: managed.module?.mvid ?? null,
|
||||
metadata_status: managed.metadata.status,
|
||||
pinvoke_imports_total: managed.pinvoke_imports.total,
|
||||
native_implementations_total: managed.native_implementations.total,
|
||||
coverage_state: managed.coverage.state,
|
||||
},
|
||||
native_observations: {
|
||||
total: input.native_observations.length,
|
||||
accepted: native.accepted,
|
||||
unsupported: native.unsupported,
|
||||
symbols: native.symbols.length,
|
||||
truncated: omittedNativeObservations > 0,
|
||||
},
|
||||
summary: {
|
||||
...counts,
|
||||
native_body_unresolved: nativeBodyUnresolved,
|
||||
},
|
||||
pinvoke_imports: [...pinvokeImports],
|
||||
native_implementations: {
|
||||
unresolved: nativeBodyUnresolved,
|
||||
reason:
|
||||
"Managed metadata tokens and RVAs are not translated to native provider addresses by this workflow; C++/CLI, ReadyToRun, and native-body mappings require explicit provider-supported bridge evidence.",
|
||||
},
|
||||
coverage: {
|
||||
status: coverageStatus,
|
||||
omitted_native_observations: omittedNativeObservations,
|
||||
omitted_candidates: omittedCandidates,
|
||||
},
|
||||
evidence_links: [
|
||||
managedEvidence.evidence_id,
|
||||
...input.native_observations
|
||||
.slice(0, input.limits.max_native_observations)
|
||||
.map(({ evidence_id: id }) => id),
|
||||
],
|
||||
limitations: [
|
||||
"P/Invoke verification checks declared import names against supplied native export or function-name Evidence only.",
|
||||
"A matching native symbol verifies that a candidate export/function was observed; it does not prove CLR binding, marshaling behavior, call reachability, or runtime loading.",
|
||||
"Missing supplied native symbols are reported as unresolved, not proof that a dependency cannot exist.",
|
||||
"Managed metadata tokens and method RVAs are not interpreted as native addresses.",
|
||||
...(managed.coverage.state === "complete"
|
||||
? []
|
||||
: ["Managed boundary input is partial or unavailable."]),
|
||||
...(native.unsupported === 0
|
||||
? []
|
||||
: [
|
||||
"Some native Evidence operations were unsupported by this workflow.",
|
||||
]),
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import { cliMetadataGuidSchema } from "./managedArtifact.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const boundedTextSchema = z.string().min(1).max(4_096);
|
||||
|
||||
const matchStatusSchema = z.enum([
|
||||
"verified",
|
||||
"inferred",
|
||||
"unresolved",
|
||||
"contradicted",
|
||||
]);
|
||||
|
||||
const matchBasisSchema = z.enum([
|
||||
"exact-export-name",
|
||||
"exact-function-name",
|
||||
"decorated-name-candidate",
|
||||
"module-mismatch",
|
||||
"no-native-candidate",
|
||||
"unsupported-native-evidence",
|
||||
]);
|
||||
|
||||
export const nativeVerificationLimitsSchema = z.strictObject({
|
||||
max_native_observations: z.number().int().min(1).max(50).default(20),
|
||||
max_candidates_per_import: z.number().int().min(1).max(100).default(25),
|
||||
});
|
||||
|
||||
export const nativeSymbolSchema = z.strictObject({
|
||||
evidence_id: evidenceIdSchema,
|
||||
operation: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
address: z.string().nullable(),
|
||||
module_name: z.string().nullable(),
|
||||
module_path: z.string().nullable(),
|
||||
source: z.enum(["macho-export", "function-dossier"]),
|
||||
});
|
||||
|
||||
export type NativeSymbol = z.infer<typeof nativeSymbolSchema>;
|
||||
|
||||
export const pinvokeVerificationSchema = z.strictObject({
|
||||
item_id: z.string().regex(/^mnv_pinvoke_[a-f0-9]{64}$/u),
|
||||
managed: z.strictObject({
|
||||
token: tokenSchema,
|
||||
member_token: tokenSchema.nullable(),
|
||||
member_name: z.string().nullable(),
|
||||
import_name: z.string().min(1),
|
||||
import_scope_name: z.string().nullable(),
|
||||
no_mangle: z.boolean(),
|
||||
char_set: z.enum(["not-specified", "ansi", "unicode", "auto", "unknown"]),
|
||||
call_convention: z.enum([
|
||||
"not-specified",
|
||||
"winapi",
|
||||
"cdecl",
|
||||
"stdcall",
|
||||
"thiscall",
|
||||
"fastcall",
|
||||
"unknown",
|
||||
]),
|
||||
declaration_verification: z.literal("managed-declaration-only"),
|
||||
}),
|
||||
status: matchStatusSchema,
|
||||
basis: matchBasisSchema,
|
||||
confidence: z.enum(["observed", "inferred", "unknown"]),
|
||||
matched_native: nativeSymbolSchema.nullable(),
|
||||
candidates: z.array(nativeSymbolSchema).max(100),
|
||||
evidence_links: z.array(evidenceIdSchema).min(1).max(51),
|
||||
limitations: z.array(boundedTextSchema).max(100),
|
||||
});
|
||||
|
||||
export type PinvokeVerification = z.infer<typeof pinvokeVerificationSchema>;
|
||||
|
||||
/** Authenticated managed boundary Evidence plus native observations. */
|
||||
export const managedNativeVerificationInputSchema = z
|
||||
.strictObject({
|
||||
managed_boundaries: evidenceSchema,
|
||||
native_observations: z.array(evidenceSchema).min(1).max(50),
|
||||
limits: nativeVerificationLimitsSchema.default({
|
||||
max_native_observations: 20,
|
||||
max_candidates_per_import: 25,
|
||||
}),
|
||||
unknown_registry_approved: z.literal(true).optional(),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
const ids = new Set<string>();
|
||||
for (const [index, evidence] of input.native_observations.entries()) {
|
||||
if (evidence.evidence_id === input.managed_boundaries.evidence_id) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["native_observations", index],
|
||||
message:
|
||||
"Native observation Evidence must be distinct from managed boundary Evidence",
|
||||
});
|
||||
}
|
||||
if (ids.has(evidence.evidence_id)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["native_observations", index],
|
||||
message: "Native observation Evidence IDs must be unique",
|
||||
});
|
||||
}
|
||||
ids.add(evidence.evidence_id);
|
||||
}
|
||||
});
|
||||
|
||||
export type ManagedNativeVerificationInput = z.infer<
|
||||
typeof managedNativeVerificationInputSchema
|
||||
>;
|
||||
|
||||
/** Provider-neutral managed/native verification result. */
|
||||
export const managedNativeVerificationResultSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
verification_id: z.string().regex(/^mnv_[a-f0-9]{64}$/u),
|
||||
algorithm: z.strictObject({
|
||||
name: z.literal("rea-managed-native-verification"),
|
||||
version: z.literal(1),
|
||||
token_identity: z.literal("build-local"),
|
||||
token_to_address_mapping: z.literal("not-inferred"),
|
||||
}),
|
||||
managed_boundary: z.strictObject({
|
||||
evidence_id: evidenceIdSchema,
|
||||
artifact_sha256: digestSchema,
|
||||
artifact_path: z.string().min(1),
|
||||
mvid: cliMetadataGuidSchema.nullable(),
|
||||
metadata_status: z.enum(["absent", "complete", "partial", "malformed"]),
|
||||
pinvoke_imports_total: z.number().int().min(0),
|
||||
native_implementations_total: z.number().int().min(0),
|
||||
coverage_state: z.enum(["complete", "partial", "unavailable"]),
|
||||
}),
|
||||
native_observations: z.strictObject({
|
||||
total: z.number().int().min(1),
|
||||
accepted: z.number().int().min(0),
|
||||
unsupported: z.number().int().min(0),
|
||||
symbols: z.number().int().min(0),
|
||||
truncated: z.boolean(),
|
||||
}),
|
||||
summary: z.strictObject({
|
||||
verified: z.number().int().min(0),
|
||||
inferred: z.number().int().min(0),
|
||||
unresolved: z.number().int().min(0),
|
||||
contradicted: z.number().int().min(0),
|
||||
native_body_unresolved: z.number().int().min(0),
|
||||
}),
|
||||
pinvoke_imports: z.array(pinvokeVerificationSchema).max(50_000),
|
||||
native_implementations: z.strictObject({
|
||||
unresolved: z.number().int().min(0),
|
||||
reason: boundedTextSchema,
|
||||
}),
|
||||
coverage: z.strictObject({
|
||||
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
|
||||
omitted_native_observations: z.number().int().min(0),
|
||||
omitted_candidates: z.number().int().min(0),
|
||||
}),
|
||||
evidence_links: z.array(evidenceIdSchema).min(2).max(51),
|
||||
limitations: z.array(boundedTextSchema).max(1_000),
|
||||
});
|
||||
|
||||
export type ManagedNativeVerificationResult = z.infer<
|
||||
typeof managedNativeVerificationResultSchema
|
||||
>;
|
||||
@@ -7,6 +7,7 @@ import { evidenceSchema, parseEvidence } from "./evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
@@ -39,6 +40,10 @@ const managedReconstructionMethodInputSchema = z.strictObject({
|
||||
}),
|
||||
});
|
||||
|
||||
type ManagedReconstructionMethodInput = z.infer<
|
||||
typeof managedReconstructionMethodInputSchema
|
||||
>;
|
||||
|
||||
export const managedReconstructionImportInputSchema = z.strictObject({
|
||||
static_members: evidenceSchema,
|
||||
decompiler: decompilerSchema,
|
||||
@@ -111,6 +116,11 @@ export type ManagedReconstructionImportResult = z.infer<
|
||||
typeof managedReconstructionImportResultSchema
|
||||
>;
|
||||
|
||||
type Member = ManagedMemberInspection["methods"]["items"][number];
|
||||
type ImportedMethod = ManagedReconstructionImportResult["methods"][number];
|
||||
type StaticObservation =
|
||||
ManagedReconstructionImportResult["static_observation"];
|
||||
|
||||
const sha256Text = (value: string): string =>
|
||||
createHash("sha256").update(value).digest("hex");
|
||||
|
||||
@@ -121,6 +131,131 @@ const sha256Json = (value: JsonValue): string => {
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
const countLines = (text: string): number =>
|
||||
text.length === 0 ? 0 : text.split(/\r\n|\r|\n/u).length;
|
||||
|
||||
const findObservedMethod = (
|
||||
members: ManagedMemberInspection,
|
||||
candidate: ManagedReconstructionMethodInput,
|
||||
): Member | undefined =>
|
||||
members.methods.items.find(
|
||||
(item) =>
|
||||
item.token === candidate.token &&
|
||||
item.signature.raw_sha256 === candidate.signature_sha256 &&
|
||||
item.body.status === "present" &&
|
||||
item.body.normalized_il_sha256 !== null &&
|
||||
candidate.normalized_il_sha256 !== null &&
|
||||
item.body.normalized_il_sha256 === candidate.normalized_il_sha256,
|
||||
);
|
||||
|
||||
const verifyTextSha256 = (
|
||||
candidate: ManagedReconstructionMethodInput,
|
||||
textSha256: string,
|
||||
): void => {
|
||||
if (
|
||||
candidate.reconstruction.text_sha256 !== undefined &&
|
||||
candidate.reconstruction.text_sha256 !== textSha256
|
||||
)
|
||||
throw new TypeError(
|
||||
`Reconstruction text hash mismatch for ${candidate.token}`,
|
||||
);
|
||||
};
|
||||
|
||||
const buildImportedMethod = (
|
||||
observed: Member,
|
||||
candidate: ManagedReconstructionMethodInput,
|
||||
textSha256: string,
|
||||
): ImportedMethod => ({
|
||||
token: observed.token,
|
||||
declaring_type: observed.declaring_type,
|
||||
name: observed.name,
|
||||
signature_sha256: observed.signature.raw_sha256,
|
||||
normalized_il_sha256: observed.body.normalized_il_sha256,
|
||||
body_sha256: observed.body.il_sha256,
|
||||
il_size: observed.body.il_size,
|
||||
reconstruction: {
|
||||
kind: candidate.reconstruction.kind,
|
||||
language: candidate.reconstruction.language,
|
||||
text: candidate.reconstruction.text,
|
||||
text_sha256: textSha256,
|
||||
text_length: candidate.reconstruction.text.length,
|
||||
line_count: countLines(candidate.reconstruction.text),
|
||||
source_path: candidate.reconstruction.source_path ?? null,
|
||||
start_line: candidate.reconstruction.start_line ?? null,
|
||||
end_line: candidate.reconstruction.end_line ?? null,
|
||||
},
|
||||
validation: {
|
||||
matched_static_member: true as const,
|
||||
exact_build_required: true as const,
|
||||
canonical_observation: false as const,
|
||||
confidence_floor: "inference" as const,
|
||||
},
|
||||
});
|
||||
|
||||
const buildReconstructionSummary = (
|
||||
methods: readonly ImportedMethod[],
|
||||
): ManagedReconstructionImportResult["summary"] => ({
|
||||
imported_methods: methods.length,
|
||||
decompiled_csharp_methods: methods.filter(
|
||||
({ reconstruction }) => reconstruction.kind === "decompiled-csharp",
|
||||
).length,
|
||||
decompiled_il_methods: methods.filter(
|
||||
({ reconstruction }) => reconstruction.kind === "decompiled-il",
|
||||
).length,
|
||||
pseudocode_methods: methods.filter(
|
||||
({ reconstruction }) => reconstruction.kind === "semantic-pseudocode",
|
||||
).length,
|
||||
total_text_bytes: methods.reduce(
|
||||
(total, { reconstruction }) => total + reconstruction.text_length,
|
||||
0,
|
||||
),
|
||||
});
|
||||
|
||||
const buildStaticObservation = (
|
||||
staticEvidence: ReturnType<typeof parseEvidence>,
|
||||
members: ManagedMemberInspection,
|
||||
): StaticObservation => ({
|
||||
evidence_id: staticEvidence.evidence_id,
|
||||
artifact_sha256: members.artifact.sha256,
|
||||
artifact_path: members.artifact.path,
|
||||
byte_length: members.artifact.byte_length,
|
||||
module_name: members.module?.name ?? null,
|
||||
mvid: members.module?.mvid ?? null,
|
||||
metadata_status: members.metadata.status,
|
||||
});
|
||||
|
||||
const buildDecompilerRecord = (
|
||||
decompiler: ManagedReconstructionImportInput["decompiler"],
|
||||
): ManagedReconstructionImportResult["decompiler"] => ({
|
||||
...decompiler,
|
||||
options_sha256: sha256Json(decompiler.options),
|
||||
});
|
||||
|
||||
const RECONSTRUCTION_LIMITATIONS = [
|
||||
"Decompiler output is imported as reconstruction evidence and analyst inference; metadata and IL observations remain canonical.",
|
||||
"This operation did not execute, load, reflect over, debug, instrument, patch, or decompile the target artifact.",
|
||||
"The imported reconstruction is valid only for the exact artifact SHA-256, MVID, method signature, and normalized IL shape recorded here.",
|
||||
"Obfuscated names, decompiler variables, and control-flow restructuring are not treated as proof without separate validation.",
|
||||
];
|
||||
|
||||
const buildReconstructionWithoutId = (
|
||||
input: ManagedReconstructionImportInput,
|
||||
staticEvidence: ReturnType<typeof parseEvidence>,
|
||||
members: ManagedMemberInspection,
|
||||
methods: readonly ImportedMethod[],
|
||||
): Omit<ManagedReconstructionImportResult, "reconstruction_id"> => ({
|
||||
schema_version: 1 as const,
|
||||
phase: "reconstruction-import" as const,
|
||||
executed: false as const,
|
||||
static_observation: buildStaticObservation(staticEvidence, members),
|
||||
decompiler: buildDecompilerRecord(input.decompiler),
|
||||
summary: buildReconstructionSummary(methods),
|
||||
methods: [...methods],
|
||||
notes: input.notes,
|
||||
evidence_links: [staticEvidence.evidence_id],
|
||||
limitations: RECONSTRUCTION_LIMITATIONS,
|
||||
});
|
||||
|
||||
/** Authenticate decompiler reconstruction against static IL Evidence. */
|
||||
export const importManagedReconstruction = (
|
||||
input: ManagedReconstructionImportInput,
|
||||
@@ -132,102 +267,23 @@ export const importManagedReconstruction = (
|
||||
staticEvidence.normalized_result,
|
||||
);
|
||||
const methods = input.methods.map((candidate) => {
|
||||
const observed = members.methods.items.find(
|
||||
(item) =>
|
||||
item.token === candidate.token &&
|
||||
item.signature.raw_sha256 === candidate.signature_sha256 &&
|
||||
item.body.status === "present" &&
|
||||
item.body.normalized_il_sha256 !== null &&
|
||||
candidate.normalized_il_sha256 !== null &&
|
||||
item.body.normalized_il_sha256 === candidate.normalized_il_sha256,
|
||||
);
|
||||
const observed = findObservedMethod(members, candidate);
|
||||
if (observed === undefined)
|
||||
throw new TypeError(
|
||||
`Reconstruction method lock ${candidate.token} does not match a static managed member observation`,
|
||||
);
|
||||
const textSha256 = sha256Text(candidate.reconstruction.text);
|
||||
if (
|
||||
candidate.reconstruction.text_sha256 !== undefined &&
|
||||
candidate.reconstruction.text_sha256 !== textSha256
|
||||
)
|
||||
throw new TypeError(
|
||||
`Reconstruction text hash mismatch for ${candidate.token}`,
|
||||
);
|
||||
return {
|
||||
token: observed.token,
|
||||
declaring_type: observed.declaring_type,
|
||||
name: observed.name,
|
||||
signature_sha256: observed.signature.raw_sha256,
|
||||
normalized_il_sha256: observed.body.normalized_il_sha256,
|
||||
body_sha256: observed.body.il_sha256,
|
||||
il_size: observed.body.il_size,
|
||||
reconstruction: {
|
||||
kind: candidate.reconstruction.kind,
|
||||
language: candidate.reconstruction.language,
|
||||
text: candidate.reconstruction.text,
|
||||
text_sha256: textSha256,
|
||||
text_length: candidate.reconstruction.text.length,
|
||||
line_count: countLines(candidate.reconstruction.text),
|
||||
source_path: candidate.reconstruction.source_path ?? null,
|
||||
start_line: candidate.reconstruction.start_line ?? null,
|
||||
end_line: candidate.reconstruction.end_line ?? null,
|
||||
},
|
||||
validation: {
|
||||
matched_static_member: true as const,
|
||||
exact_build_required: true as const,
|
||||
canonical_observation: false as const,
|
||||
confidence_floor: "inference" as const,
|
||||
},
|
||||
};
|
||||
verifyTextSha256(candidate, textSha256);
|
||||
return buildImportedMethod(observed, candidate, textSha256);
|
||||
});
|
||||
const withoutId = {
|
||||
schema_version: 1 as const,
|
||||
phase: "reconstruction-import" as const,
|
||||
executed: false as const,
|
||||
static_observation: {
|
||||
evidence_id: staticEvidence.evidence_id,
|
||||
artifact_sha256: members.artifact.sha256,
|
||||
artifact_path: members.artifact.path,
|
||||
byte_length: members.artifact.byte_length,
|
||||
module_name: members.module?.name ?? null,
|
||||
mvid: members.module?.mvid ?? null,
|
||||
metadata_status: members.metadata.status,
|
||||
},
|
||||
decompiler: {
|
||||
...input.decompiler,
|
||||
options_sha256: sha256Json(input.decompiler.options),
|
||||
},
|
||||
summary: {
|
||||
imported_methods: methods.length,
|
||||
decompiled_csharp_methods: methods.filter(
|
||||
({ reconstruction }) => reconstruction.kind === "decompiled-csharp",
|
||||
).length,
|
||||
decompiled_il_methods: methods.filter(
|
||||
({ reconstruction }) => reconstruction.kind === "decompiled-il",
|
||||
).length,
|
||||
pseudocode_methods: methods.filter(
|
||||
({ reconstruction }) => reconstruction.kind === "semantic-pseudocode",
|
||||
).length,
|
||||
total_text_bytes: methods.reduce(
|
||||
(total, { reconstruction }) => total + reconstruction.text_length,
|
||||
0,
|
||||
),
|
||||
},
|
||||
const withoutId = buildReconstructionWithoutId(
|
||||
input,
|
||||
staticEvidence,
|
||||
members,
|
||||
methods,
|
||||
notes: input.notes,
|
||||
evidence_links: [staticEvidence.evidence_id],
|
||||
limitations: [
|
||||
"Decompiler output is imported as reconstruction evidence and analyst inference; metadata and IL observations remain canonical.",
|
||||
"This operation did not execute, load, reflect over, debug, instrument, patch, or decompile the target artifact.",
|
||||
"The imported reconstruction is valid only for the exact artifact SHA-256, MVID, method signature, and normalized IL shape recorded here.",
|
||||
"Obfuscated names, decompiler variables, and control-flow restructuring are not treated as proof without separate validation.",
|
||||
],
|
||||
};
|
||||
);
|
||||
return managedReconstructionImportResultSchema.parse({
|
||||
...withoutId,
|
||||
reconstruction_id: `mre_${sha256Json(withoutId)}`,
|
||||
});
|
||||
};
|
||||
|
||||
const countLines = (text: string): number =>
|
||||
text.length === 0 ? 0 : text.split(/\r\n|\r|\n/u).length;
|
||||
|
||||
@@ -161,16 +161,21 @@ const unavailableResult = (
|
||||
});
|
||||
};
|
||||
|
||||
const runtimeClassification = (
|
||||
layout: ManagedPeLayout,
|
||||
metadata: ManagedMetadataLayout,
|
||||
interface RuntimeClassificationInput {
|
||||
readonly layout: ManagedPeLayout;
|
||||
readonly metadata: ManagedMetadataLayout;
|
||||
readonly inventory: ManagedMetadataInventory;
|
||||
}
|
||||
|
||||
const runtimeFamilyFor = (
|
||||
cli: ManagedCliHeader,
|
||||
inventory: ManagedMetadataInventory,
|
||||
metadata: ManagedMetadataLayout,
|
||||
layout: ManagedPeLayout,
|
||||
): Pick<
|
||||
ManagedArtifactInspection["classification"],
|
||||
"runtime_family" | "implementation" | "managed_architecture" | "evidence"
|
||||
"runtime_family" | "evidence"
|
||||
> => {
|
||||
const cli = layout.cli;
|
||||
if (cli === null) throw new TypeError("Managed classification requires CLI");
|
||||
const evidence: ManagedArtifactInspection["classification"]["evidence"] = [
|
||||
{
|
||||
code: "cli-header-observed",
|
||||
@@ -232,22 +237,47 @@ const runtimeClassification = (
|
||||
: (metadata.table(12)?.offset ?? null),
|
||||
});
|
||||
}
|
||||
const implementation =
|
||||
(cli.flags & 1) === 0
|
||||
? ("cpp-cli-mixed" as const)
|
||||
: cli.readyToRunSignature
|
||||
? ("cil-and-ready-to-run" as const)
|
||||
: (metadata.rowCounts[6] ?? 0) === 0
|
||||
? ("metadata-only" as const)
|
||||
: ("cil" as const);
|
||||
const managedArchitecture =
|
||||
layout.architecture === "x86" && (cli.flags & 1) !== 0
|
||||
? (cli.flags & 0x0000_0002) !== 0
|
||||
? "x86"
|
||||
: (cli.flags & 0x0002_0000) !== 0
|
||||
? "anycpu-prefer-32"
|
||||
: "anycpu"
|
||||
: layout.architecture;
|
||||
return { runtime_family: runtimeFamily, evidence };
|
||||
};
|
||||
|
||||
const implementationFor = (
|
||||
cli: ManagedCliHeader,
|
||||
metadata: ManagedMetadataLayout,
|
||||
): ManagedArtifactInspection["classification"]["implementation"] => {
|
||||
if ((cli.flags & 1) === 0) return "cpp-cli-mixed";
|
||||
if (cli.readyToRunSignature) return "cil-and-ready-to-run";
|
||||
return (metadata.rowCounts[6] ?? 0) === 0 ? "metadata-only" : "cil";
|
||||
};
|
||||
|
||||
const managedArchitectureFor = (
|
||||
layout: ManagedPeLayout,
|
||||
cli: ManagedCliHeader,
|
||||
): ManagedArtifactInspection["classification"]["managed_architecture"] => {
|
||||
if (layout.architecture !== "x86" || (cli.flags & 1) === 0)
|
||||
return layout.architecture;
|
||||
if ((cli.flags & 0x0000_0002) !== 0) return "x86";
|
||||
if ((cli.flags & 0x0002_0000) !== 0) return "anycpu-prefer-32";
|
||||
return "anycpu";
|
||||
};
|
||||
|
||||
const runtimeClassification = ({
|
||||
layout,
|
||||
metadata,
|
||||
inventory,
|
||||
}: RuntimeClassificationInput): Pick<
|
||||
ManagedArtifactInspection["classification"],
|
||||
"runtime_family" | "implementation" | "managed_architecture" | "evidence"
|
||||
> => {
|
||||
const cli = layout.cli;
|
||||
if (cli === null) throw new TypeError("Managed classification requires CLI");
|
||||
const { runtime_family: runtimeFamily, evidence } = runtimeFamilyFor(
|
||||
cli,
|
||||
inventory,
|
||||
metadata,
|
||||
layout,
|
||||
);
|
||||
const implementation = implementationFor(cli, metadata);
|
||||
const managedArchitecture = managedArchitectureFor(layout, cli);
|
||||
evidence.push({
|
||||
code: "managed-architecture-derived",
|
||||
detail: `Managed architecture ${managedArchitecture} is derived from PE machine and CLI flags.`,
|
||||
@@ -261,13 +291,21 @@ const runtimeClassification = (
|
||||
};
|
||||
};
|
||||
|
||||
const partialMetadataResult = (
|
||||
target: BinaryTarget,
|
||||
bytes: Buffer,
|
||||
layout: ManagedPeLayout,
|
||||
limits: ManagedInspectionLimits,
|
||||
issue: ManagedParseIssue,
|
||||
): ManagedArtifactInspection => ({
|
||||
interface PartialMetadataContext {
|
||||
readonly target: BinaryTarget;
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedPeLayout;
|
||||
readonly limits: ManagedInspectionLimits;
|
||||
readonly issue: ManagedParseIssue;
|
||||
}
|
||||
|
||||
const partialMetadataResult = ({
|
||||
target,
|
||||
bytes,
|
||||
layout,
|
||||
limits,
|
||||
issue,
|
||||
}: PartialMetadataContext): ManagedArtifactInspection => ({
|
||||
...base(target, bytes, layout),
|
||||
classification: {
|
||||
status: "malformed",
|
||||
@@ -338,11 +376,17 @@ export const inspectManagedArtifactBytes = (
|
||||
const cli = layout.cli;
|
||||
if (cli.metadata.size > limits.maxMetadataBytes)
|
||||
return managedArtifactInspectionSchema.parse(
|
||||
partialMetadataResult(target, bytes, layout, limits, {
|
||||
code: "limit-exceeded",
|
||||
scope: "cli.metadata",
|
||||
offset: cli.headerOffset + 8,
|
||||
detail: `CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
|
||||
partialMetadataResult({
|
||||
target,
|
||||
bytes,
|
||||
layout,
|
||||
limits,
|
||||
issue: {
|
||||
code: "limit-exceeded",
|
||||
scope: "cli.metadata",
|
||||
offset: cli.headerOffset + 8,
|
||||
detail: `CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
let metadata: ManagedMetadataLayout;
|
||||
@@ -361,7 +405,7 @@ export const inspectManagedArtifactBytes = (
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
return managedArtifactInspectionSchema.parse(
|
||||
partialMetadataResult(target, bytes, layout, limits, cause.issue),
|
||||
partialMetadataResult({ target, bytes, layout, limits, issue: cause.issue }),
|
||||
);
|
||||
}
|
||||
const resourceIssues: ManagedParseIssue[] = [];
|
||||
@@ -395,7 +439,7 @@ export const inspectManagedArtifactBytes = (
|
||||
classification: {
|
||||
status: "managed",
|
||||
container: "pe",
|
||||
...runtimeClassification(layout, metadata, inventory),
|
||||
...runtimeClassification({ layout, metadata, inventory }),
|
||||
},
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
|
||||
@@ -4,23 +4,18 @@ import type {
|
||||
} from "../domain/managedArtifact.js";
|
||||
import {
|
||||
METADATA_TABLE_NAMES,
|
||||
metadataRowOffset,
|
||||
type ManagedMetadataLayout,
|
||||
type MetadataTableLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
MetadataRowCursor,
|
||||
metadataToken,
|
||||
readMetadataBlob,
|
||||
readMetadataGuid,
|
||||
readMetadataString,
|
||||
sha256Bytes,
|
||||
strongNameToken,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import {
|
||||
ManagedReaderFailure,
|
||||
managedFailure,
|
||||
} from "./ManagedReaderFailure.js";
|
||||
readAssembly,
|
||||
readAssemblyReference,
|
||||
readCustomAttribute,
|
||||
readModule,
|
||||
readResource,
|
||||
} from "./ManagedMetadataInventoryRows.js";
|
||||
import { metadataToken } from "./ManagedMetadataHeaps.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
|
||||
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
|
||||
type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
|
||||
@@ -64,469 +59,6 @@ export interface ManagedMetadataInventory {
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
}
|
||||
|
||||
const rowCursor = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
table: number,
|
||||
row: number,
|
||||
): MetadataRowCursor => {
|
||||
const descriptor = layout.table(table);
|
||||
const start = metadataRowOffset(layout, table, row);
|
||||
if (descriptor === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-row",
|
||||
`metadata.${String(table)}`,
|
||||
"Metadata table is absent",
|
||||
start,
|
||||
);
|
||||
return new MetadataRowCursor(
|
||||
bytes,
|
||||
start,
|
||||
start + descriptor.rowSize,
|
||||
`metadata.${descriptor.name}`,
|
||||
);
|
||||
};
|
||||
|
||||
const publicKeyIdentity = (
|
||||
bytes: Buffer,
|
||||
kind: "public-key" | "public-key-token",
|
||||
): AssemblyIdentity["public_key"] => ({
|
||||
kind: bytes.length === 0 ? "none" : kind,
|
||||
byte_length: bytes.length,
|
||||
sha256: bytes.length === 0 ? null : sha256Bytes(bytes),
|
||||
token:
|
||||
bytes.length === 0
|
||||
? null
|
||||
: kind === "public-key"
|
||||
? strongNameToken(bytes)
|
||||
: bytes.length === 8
|
||||
? bytes.toString("hex")
|
||||
: null,
|
||||
});
|
||||
|
||||
const readModule = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): ModuleIdentity | null => {
|
||||
const table = layout.table(0);
|
||||
if (table === undefined || table.rowCount === 0) return null;
|
||||
const cursor = rowCursor(bytes, layout, 0, 1);
|
||||
const generation = cursor.readUInt16();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const mvid = readMetadataGuid(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.guidIndexSize),
|
||||
);
|
||||
const encId = readMetadataGuid(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.guidIndexSize),
|
||||
);
|
||||
const encBaseId = readMetadataGuid(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.guidIndexSize),
|
||||
);
|
||||
return {
|
||||
name,
|
||||
generation,
|
||||
mvid,
|
||||
enc_id: encId,
|
||||
enc_base_id: encBaseId,
|
||||
token: metadataToken(0, 1),
|
||||
row_offset: table.offset,
|
||||
};
|
||||
};
|
||||
|
||||
const readAssembly = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): AssemblyIdentity | null => {
|
||||
const table = layout.table(32);
|
||||
if (table === undefined || table.rowCount === 0) return null;
|
||||
const cursor = rowCursor(bytes, layout, 32, 1);
|
||||
const hashAlgorithm = cursor.readUInt32();
|
||||
const version = [
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
].join(".");
|
||||
const flags = cursor.readUInt32();
|
||||
const key = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const culture = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
culture: culture.length === 0 ? null : culture,
|
||||
flags,
|
||||
hash_algorithm: hashAlgorithm,
|
||||
public_key: publicKeyIdentity(
|
||||
key,
|
||||
(flags & 1) === 0 ? "public-key-token" : "public-key",
|
||||
),
|
||||
token: metadataToken(32, 1),
|
||||
row_offset: table.offset,
|
||||
};
|
||||
};
|
||||
|
||||
const readAssemblyReference = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
maxBytes: number,
|
||||
): AssemblyReference => {
|
||||
const cursor = rowCursor(bytes, layout, 35, row);
|
||||
const version = [
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
].join(".");
|
||||
const flags = cursor.readUInt32();
|
||||
const keyOrToken = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const culture = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const hashValue = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
culture: culture.length === 0 ? null : culture,
|
||||
flags,
|
||||
public_key_or_token: publicKeyIdentity(
|
||||
keyOrToken,
|
||||
(flags & 1) === 0 ? "public-key-token" : "public-key",
|
||||
),
|
||||
hash_value_sha256: hashValue.length === 0 ? null : sha256Bytes(hashValue),
|
||||
hash_value_length: hashValue.length,
|
||||
token: metadataToken(35, row),
|
||||
row_offset: metadataRowOffset(layout, 35, row),
|
||||
};
|
||||
};
|
||||
|
||||
const codedToken = (
|
||||
raw: number,
|
||||
bits: number,
|
||||
tables: readonly (number | undefined)[],
|
||||
): string | null => {
|
||||
if (raw === 0) return null;
|
||||
const tag = raw & (2 ** bits - 1);
|
||||
const row = Math.floor(raw / 2 ** bits);
|
||||
const table = tables[tag];
|
||||
return table === undefined || row === 0 ? null : metadataToken(table, row);
|
||||
};
|
||||
|
||||
const readTypeName = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
table: 1 | 2,
|
||||
row: number,
|
||||
maxBytes: number,
|
||||
): string | null => {
|
||||
const descriptor = layout.table(table);
|
||||
if (descriptor === undefined || row < 1 || row > descriptor.rowCount)
|
||||
return null;
|
||||
const cursor = rowCursor(bytes, layout, table, row);
|
||||
if (table === 1) cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
|
||||
else cursor.readUInt32();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const namespace = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
return namespace.length === 0 ? name : `${namespace}.${name}`;
|
||||
};
|
||||
|
||||
const declaringTypeForMethod = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
methodRow: number,
|
||||
maxBytes: number,
|
||||
): string | null => {
|
||||
const types = layout.table(2);
|
||||
const methods = layout.table(6);
|
||||
if (types === undefined || methods === undefined) return null;
|
||||
for (let row = 1; row <= types.rowCount; row += 1) {
|
||||
const cursor = rowCursor(bytes, layout, 2, row);
|
||||
cursor.readUInt32();
|
||||
const nameIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
cursor.readIndex(layout.tableIndexSize(4));
|
||||
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
|
||||
const methodEnd =
|
||||
row === types.rowCount
|
||||
? methods.rowCount + 1
|
||||
: methodListForType(bytes, layout, row + 1);
|
||||
if (methodRow < methodStart || methodRow >= methodEnd) continue;
|
||||
const name = readMetadataString(bytes, layout, nameIndex, maxBytes);
|
||||
const namespace = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
namespaceIndex,
|
||||
maxBytes,
|
||||
);
|
||||
return namespace.length === 0 ? name : `${namespace}.${name}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const methodListForType = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
): number => {
|
||||
const cursor = rowCursor(bytes, layout, 2, row);
|
||||
cursor.readUInt32();
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
cursor.readIndex(layout.tableIndexSize(4));
|
||||
return cursor.readIndex(layout.tableIndexSize(6));
|
||||
};
|
||||
|
||||
const attributeTypeName = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
rawType: number,
|
||||
maxBytes: number,
|
||||
): string | null => {
|
||||
const tag = rawType & 7;
|
||||
const row = Math.floor(rawType / 8);
|
||||
if (row === 0) return null;
|
||||
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, maxBytes);
|
||||
if (tag !== 3) return null;
|
||||
const memberRefs = layout.table(10);
|
||||
if (memberRefs === undefined || row > memberRefs.rowCount) return null;
|
||||
const cursor = rowCursor(bytes, layout, 10, row);
|
||||
const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
|
||||
const parentTag = parent & 7;
|
||||
const parentRow = Math.floor(parent / 8);
|
||||
if (parentTag === 0)
|
||||
return readTypeName(bytes, layout, 2, parentRow, maxBytes);
|
||||
if (parentTag === 1)
|
||||
return readTypeName(bytes, layout, 1, parentRow, maxBytes);
|
||||
if (parentTag === 3)
|
||||
return declaringTypeForMethod(bytes, layout, parentRow, maxBytes);
|
||||
return null;
|
||||
};
|
||||
|
||||
const decodeCompressedLength = (
|
||||
blob: Buffer,
|
||||
offset: number,
|
||||
): { readonly length: number; readonly prefix: number } | undefined => {
|
||||
const first = blob[offset];
|
||||
if (first === undefined) return undefined;
|
||||
if ((first & 0x80) === 0) return { length: first, prefix: 1 };
|
||||
if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined)
|
||||
return {
|
||||
length: ((first & 0x3f) << 8) | (blob[offset + 1] ?? 0),
|
||||
prefix: 2,
|
||||
};
|
||||
if (
|
||||
(first & 0xe0) === 0xc0 &&
|
||||
blob[offset + 1] !== undefined &&
|
||||
blob[offset + 2] !== undefined &&
|
||||
blob[offset + 3] !== undefined
|
||||
)
|
||||
return {
|
||||
length:
|
||||
(first & 0x1f) * 0x01_00_00_00 +
|
||||
(blob[offset + 1] ?? 0) * 0x01_00_00 +
|
||||
(blob[offset + 2] ?? 0) * 0x0100 +
|
||||
(blob[offset + 3] ?? 0),
|
||||
prefix: 4,
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const decodeFixedString = (blob: Buffer): string | null => {
|
||||
if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff)
|
||||
return null;
|
||||
const length = decodeCompressedLength(blob, 2);
|
||||
if (length === undefined) return null;
|
||||
const start = 2 + length.prefix;
|
||||
if (start > blob.length - length.length) return null;
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
blob.subarray(start, start + length.length),
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const readCustomAttribute = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
maxBytes: number,
|
||||
): CustomAttribute => {
|
||||
const cursor = rowCursor(bytes, layout, 12, row);
|
||||
const parentRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("HasCustomAttribute"),
|
||||
);
|
||||
const typeRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("CustomAttributeType"),
|
||||
);
|
||||
const value = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const parent = codedToken(
|
||||
parentRaw,
|
||||
5,
|
||||
[
|
||||
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
|
||||
44, 43,
|
||||
],
|
||||
);
|
||||
if (parent === null)
|
||||
throw managedFailure(
|
||||
"invalid-row",
|
||||
"metadata.CustomAttribute",
|
||||
"CustomAttribute parent coded index is invalid",
|
||||
cursor.start,
|
||||
);
|
||||
const typeName = attributeTypeName(bytes, layout, typeRaw, maxBytes);
|
||||
return {
|
||||
parent_token: parent,
|
||||
constructor_token: codedToken(typeRaw, 3, [undefined, undefined, 6, 10]),
|
||||
type_name: typeName,
|
||||
value_length: value.length,
|
||||
value_sha256: sha256Bytes(value),
|
||||
decoded_fixed_string:
|
||||
typeName === "System.Runtime.Versioning.TargetFrameworkAttribute"
|
||||
? decodeFixedString(value)
|
||||
: null,
|
||||
token: metadataToken(12, row),
|
||||
row_offset: cursor.start,
|
||||
};
|
||||
};
|
||||
|
||||
const readResource = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
maxBytes: number,
|
||||
directory: ManagedResourceDirectory | null,
|
||||
issues: ManagedParseIssue[],
|
||||
): ManagedResource => {
|
||||
const cursor = rowCursor(bytes, layout, 40, row);
|
||||
const declaredOffset = cursor.readUInt32();
|
||||
const flags = cursor.readUInt32();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const implementationRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("Implementation"),
|
||||
);
|
||||
const implementationToken = codedToken(implementationRaw, 2, [38, 35, 39]);
|
||||
let dataLength: number | null = null;
|
||||
let dataSha256: string | null = null;
|
||||
if (implementationRaw === 0) {
|
||||
if (directory === null || declaredOffset > directory.size - 4) {
|
||||
issues.push({
|
||||
code: "invalid-resource",
|
||||
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
|
||||
offset: directory?.offset ?? null,
|
||||
detail: "Embedded resource offset leaves the CLI resources directory",
|
||||
});
|
||||
} else {
|
||||
const start = directory.offset + declaredOffset;
|
||||
dataLength = bytes.readUInt32LE(start);
|
||||
if (dataLength > directory.size - declaredOffset - 4) {
|
||||
issues.push({
|
||||
code: "invalid-resource",
|
||||
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
|
||||
offset: start,
|
||||
detail: "Embedded resource length leaves the CLI resources directory",
|
||||
});
|
||||
} else if (dataLength > maxBytes) {
|
||||
issues.push({
|
||||
code: "limit-exceeded",
|
||||
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
|
||||
offset: start + 4,
|
||||
detail: `Embedded resource exceeds max_heap_item_bytes ${String(maxBytes)}; digest omitted`,
|
||||
});
|
||||
} else {
|
||||
dataSha256 = sha256Bytes(
|
||||
bytes.subarray(start + 4, start + 4 + dataLength),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name,
|
||||
flags,
|
||||
visibility:
|
||||
(flags & 7) === 1 ? "public" : (flags & 7) === 2 ? "private" : "unknown",
|
||||
implementation_token: implementationToken,
|
||||
embedded: implementationRaw === 0,
|
||||
declared_offset: declaredOffset,
|
||||
data_length: dataLength,
|
||||
data_sha256: dataSha256,
|
||||
token: metadataToken(40, row),
|
||||
row_offset: cursor.start,
|
||||
};
|
||||
};
|
||||
|
||||
const safeRead = <Value>(
|
||||
operation: () => Value,
|
||||
issues: ManagedParseIssue[],
|
||||
@@ -570,6 +102,111 @@ const uniqueIssues = (
|
||||
...new Map(issues.map((issue) => [JSON.stringify(issue), issue])).values(),
|
||||
];
|
||||
|
||||
const readPagedReferences = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
input: ManagedInventoryInput,
|
||||
issues: ManagedParseIssue[],
|
||||
): ManagedPage<AssemblyReference> =>
|
||||
pageRows(
|
||||
layout.table(35),
|
||||
input.referenceOffset,
|
||||
input.referenceLimit,
|
||||
(row) =>
|
||||
safeRead(
|
||||
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
|
||||
issues,
|
||||
),
|
||||
);
|
||||
|
||||
const readPagedResources = ({
|
||||
bytes,
|
||||
layout,
|
||||
input,
|
||||
resourceDirectory,
|
||||
issues,
|
||||
}: {
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly input: ManagedInventoryInput;
|
||||
readonly resourceDirectory: ManagedResourceDirectory | null;
|
||||
readonly issues: ManagedParseIssue[];
|
||||
}): ManagedPage<ManagedResource> =>
|
||||
pageRows(layout.table(40), input.resourceOffset, input.resourceLimit, (row) =>
|
||||
safeRead(
|
||||
() =>
|
||||
readResource({
|
||||
bytes,
|
||||
layout,
|
||||
row,
|
||||
maxBytes: input.maxHeapItemBytes,
|
||||
directory: resourceDirectory,
|
||||
issues,
|
||||
}),
|
||||
issues,
|
||||
),
|
||||
);
|
||||
|
||||
const readPagedAttributes = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
input: ManagedInventoryInput,
|
||||
issues: ManagedParseIssue[],
|
||||
): ManagedPage<CustomAttribute> =>
|
||||
pageRows(
|
||||
layout.table(12),
|
||||
input.attributeOffset,
|
||||
input.attributeLimit,
|
||||
(row) =>
|
||||
safeRead(
|
||||
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
|
||||
issues,
|
||||
),
|
||||
);
|
||||
|
||||
const collectReferenceNames = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
issues: ManagedParseIssue[],
|
||||
): readonly string[] => {
|
||||
const names: string[] = [];
|
||||
const referenceTable = layout.table(35);
|
||||
for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) {
|
||||
const reference = safeRead(
|
||||
() => readAssemblyReference(bytes, layout, row, maxBytes),
|
||||
issues,
|
||||
);
|
||||
if (reference !== undefined) names.push(reference.name);
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
const collectTargetFrameworks = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
issues: ManagedParseIssue[],
|
||||
): readonly string[] => {
|
||||
const targetFrameworks: string[] = [];
|
||||
const attributeTable = layout.table(12);
|
||||
for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) {
|
||||
const attribute = safeRead(
|
||||
() => readCustomAttribute(bytes, layout, row, maxBytes),
|
||||
issues,
|
||||
);
|
||||
if (
|
||||
attribute?.parent_token === metadataToken(32, 1) &&
|
||||
attribute.type_name ===
|
||||
"System.Runtime.Versioning.TargetFrameworkAttribute" &&
|
||||
attribute.decoded_fixed_string !== null &&
|
||||
!targetFrameworks.includes(attribute.decoded_fixed_string)
|
||||
)
|
||||
targetFrameworks.push(attribute.decoded_fixed_string);
|
||||
}
|
||||
return targetFrameworks;
|
||||
};
|
||||
|
||||
/** Inventory bounded identity tables without CLR reflection or execution. */
|
||||
export const readManagedMetadataInventory = (
|
||||
bytes: Buffer,
|
||||
@@ -586,73 +223,31 @@ export const readManagedMetadataInventory = (
|
||||
() => readAssembly(bytes, layout, input.maxHeapItemBytes),
|
||||
issues,
|
||||
) ?? null;
|
||||
const references = pageRows(
|
||||
layout.table(35),
|
||||
input.referenceOffset,
|
||||
input.referenceLimit,
|
||||
(row) =>
|
||||
safeRead(
|
||||
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
|
||||
issues,
|
||||
),
|
||||
const references = readPagedReferences(bytes, layout, input, issues);
|
||||
const resources = readPagedResources({
|
||||
bytes,
|
||||
layout,
|
||||
input,
|
||||
resourceDirectory,
|
||||
issues,
|
||||
});
|
||||
const attributes = readPagedAttributes(bytes, layout, input, issues);
|
||||
const referenceNames = collectReferenceNames(
|
||||
bytes,
|
||||
layout,
|
||||
input.maxHeapItemBytes,
|
||||
issues,
|
||||
);
|
||||
const resources = pageRows(
|
||||
layout.table(40),
|
||||
input.resourceOffset,
|
||||
input.resourceLimit,
|
||||
(row) =>
|
||||
safeRead(
|
||||
() =>
|
||||
readResource(
|
||||
bytes,
|
||||
layout,
|
||||
row,
|
||||
input.maxHeapItemBytes,
|
||||
resourceDirectory,
|
||||
issues,
|
||||
),
|
||||
issues,
|
||||
),
|
||||
const targetFrameworks = collectTargetFrameworks(
|
||||
bytes,
|
||||
layout,
|
||||
input.maxHeapItemBytes,
|
||||
issues,
|
||||
);
|
||||
const attributes = pageRows(
|
||||
layout.table(12),
|
||||
input.attributeOffset,
|
||||
input.attributeLimit,
|
||||
(row) =>
|
||||
safeRead(
|
||||
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
|
||||
issues,
|
||||
),
|
||||
);
|
||||
const referenceNames: string[] = [];
|
||||
const referenceTable = layout.table(35);
|
||||
for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) {
|
||||
const reference = safeRead(
|
||||
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
|
||||
issues,
|
||||
);
|
||||
if (reference !== undefined) referenceNames.push(reference.name);
|
||||
}
|
||||
const targetFrameworks: string[] = [];
|
||||
const attributeTable = layout.table(12);
|
||||
for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) {
|
||||
const attribute = safeRead(
|
||||
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
|
||||
issues,
|
||||
);
|
||||
if (
|
||||
attribute?.parent_token === metadataToken(32, 1) &&
|
||||
attribute.type_name ===
|
||||
"System.Runtime.Versioning.TargetFrameworkAttribute" &&
|
||||
attribute.decoded_fixed_string !== null &&
|
||||
!targetFrameworks.includes(attribute.decoded_fixed_string)
|
||||
)
|
||||
targetFrameworks.push(attribute.decoded_fixed_string);
|
||||
}
|
||||
return {
|
||||
module,
|
||||
assembly,
|
||||
targetFrameworks: targetFrameworks.sort(),
|
||||
targetFrameworks: [...targetFrameworks].sort(),
|
||||
referenceNames: [...new Set(referenceNames)].sort(),
|
||||
references,
|
||||
resources,
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
import type {
|
||||
ManagedArtifactInspection,
|
||||
ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
import {
|
||||
metadataRowOffset,
|
||||
type ManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
MetadataRowCursor,
|
||||
metadataToken,
|
||||
readMetadataBlob,
|
||||
readMetadataGuid,
|
||||
readMetadataString,
|
||||
sha256Bytes,
|
||||
strongNameToken,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js";
|
||||
|
||||
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
|
||||
type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
|
||||
type AssemblyReference =
|
||||
ManagedArtifactInspection["references"]["items"][number];
|
||||
type ManagedResource = ManagedArtifactInspection["resources"]["items"][number];
|
||||
type CustomAttribute = ManagedArtifactInspection["attributes"]["items"][number];
|
||||
|
||||
interface RowCursorContext {
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly table: number;
|
||||
readonly row: number;
|
||||
}
|
||||
|
||||
const rowCursor = ({
|
||||
bytes,
|
||||
layout,
|
||||
table,
|
||||
row,
|
||||
}: RowCursorContext): MetadataRowCursor => {
|
||||
const descriptor = layout.table(table);
|
||||
const start = metadataRowOffset(layout, table, row);
|
||||
if (descriptor === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-row",
|
||||
`metadata.${String(table)}`,
|
||||
"Metadata table is absent",
|
||||
start,
|
||||
);
|
||||
return new MetadataRowCursor(
|
||||
bytes,
|
||||
start,
|
||||
start + descriptor.rowSize,
|
||||
`metadata.${descriptor.name}`,
|
||||
);
|
||||
};
|
||||
|
||||
const publicKeyIdentity = (
|
||||
bytes: Buffer,
|
||||
kind: "public-key" | "public-key-token",
|
||||
): AssemblyIdentity["public_key"] => ({
|
||||
kind: bytes.length === 0 ? "none" : kind,
|
||||
byte_length: bytes.length,
|
||||
sha256: bytes.length === 0 ? null : sha256Bytes(bytes),
|
||||
token:
|
||||
bytes.length === 0
|
||||
? null
|
||||
: kind === "public-key"
|
||||
? strongNameToken(bytes)
|
||||
: bytes.length === 8
|
||||
? bytes.toString("hex")
|
||||
: null,
|
||||
});
|
||||
|
||||
export const readModule = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): ModuleIdentity | null => {
|
||||
const table = layout.table(0);
|
||||
if (table === undefined || table.rowCount === 0) return null;
|
||||
const cursor = rowCursor({ bytes, layout, table: 0, row: 1 });
|
||||
const generation = cursor.readUInt16();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const mvid = readMetadataGuid(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.guidIndexSize),
|
||||
);
|
||||
const encId = readMetadataGuid(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.guidIndexSize),
|
||||
);
|
||||
const encBaseId = readMetadataGuid(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.guidIndexSize),
|
||||
);
|
||||
return {
|
||||
name,
|
||||
generation,
|
||||
mvid,
|
||||
enc_id: encId,
|
||||
enc_base_id: encBaseId,
|
||||
token: metadataToken(0, 1),
|
||||
row_offset: table.offset,
|
||||
};
|
||||
};
|
||||
|
||||
export const readAssembly = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): AssemblyIdentity | null => {
|
||||
const table = layout.table(32);
|
||||
if (table === undefined || table.rowCount === 0) return null;
|
||||
const cursor = rowCursor({ bytes, layout, table: 32, row: 1 });
|
||||
const hashAlgorithm = cursor.readUInt32();
|
||||
const version = [
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
].join(".");
|
||||
const flags = cursor.readUInt32();
|
||||
const key = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const culture = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
culture: culture.length === 0 ? null : culture,
|
||||
flags,
|
||||
hash_algorithm: hashAlgorithm,
|
||||
public_key: publicKeyIdentity(
|
||||
key,
|
||||
(flags & 1) === 0 ? "public-key-token" : "public-key",
|
||||
),
|
||||
token: metadataToken(32, 1),
|
||||
row_offset: table.offset,
|
||||
};
|
||||
};
|
||||
|
||||
export const readAssemblyReference = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
maxBytes: number,
|
||||
): AssemblyReference => {
|
||||
const cursor = rowCursor({ bytes, layout, table: 35, row });
|
||||
const version = [
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
cursor.readUInt16(),
|
||||
].join(".");
|
||||
const flags = cursor.readUInt32();
|
||||
const keyOrToken = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const culture = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const hashValue = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
culture: culture.length === 0 ? null : culture,
|
||||
flags,
|
||||
public_key_or_token: publicKeyIdentity(
|
||||
keyOrToken,
|
||||
(flags & 1) === 0 ? "public-key-token" : "public-key",
|
||||
),
|
||||
hash_value_sha256: hashValue.length === 0 ? null : sha256Bytes(hashValue),
|
||||
hash_value_length: hashValue.length,
|
||||
token: metadataToken(35, row),
|
||||
row_offset: metadataRowOffset(layout, 35, row),
|
||||
};
|
||||
};
|
||||
|
||||
const codedToken = (
|
||||
raw: number,
|
||||
bits: number,
|
||||
tables: readonly (number | undefined)[],
|
||||
): string | null => {
|
||||
if (raw === 0) return null;
|
||||
const tag = raw & (2 ** bits - 1);
|
||||
const row = Math.floor(raw / 2 ** bits);
|
||||
const table = tables[tag];
|
||||
return table === undefined || row === 0 ? null : metadataToken(table, row);
|
||||
};
|
||||
|
||||
interface ReadTypeNameContext {
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly table: 1 | 2;
|
||||
readonly row: number;
|
||||
readonly maxBytes: number;
|
||||
}
|
||||
|
||||
const readTypeName = ({
|
||||
bytes,
|
||||
layout,
|
||||
table,
|
||||
row,
|
||||
maxBytes,
|
||||
}: ReadTypeNameContext): string | null => {
|
||||
const descriptor = layout.table(table);
|
||||
if (descriptor === undefined || row < 1 || row > descriptor.rowCount)
|
||||
return null;
|
||||
const cursor = rowCursor({ bytes, layout, table, row });
|
||||
if (table === 1) cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
|
||||
else cursor.readUInt32();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const namespace = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
return namespace.length === 0 ? name : `${namespace}.${name}`;
|
||||
};
|
||||
|
||||
const declaringTypeForMethod = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
methodRow: number,
|
||||
maxBytes: number,
|
||||
): string | null => {
|
||||
const types = layout.table(2);
|
||||
const methods = layout.table(6);
|
||||
if (types === undefined || methods === undefined) return null;
|
||||
for (let row = 1; row <= types.rowCount; row += 1) {
|
||||
const cursor = rowCursor({ bytes, layout, table: 2, row });
|
||||
cursor.readUInt32();
|
||||
const nameIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
cursor.readIndex(layout.tableIndexSize(4));
|
||||
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
|
||||
const methodEnd =
|
||||
row === types.rowCount
|
||||
? methods.rowCount + 1
|
||||
: methodListForType(bytes, layout, row + 1);
|
||||
if (methodRow < methodStart || methodRow >= methodEnd) continue;
|
||||
const name = readMetadataString(bytes, layout, nameIndex, maxBytes);
|
||||
const namespace = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
namespaceIndex,
|
||||
maxBytes,
|
||||
);
|
||||
return namespace.length === 0 ? name : `${namespace}.${name}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const methodListForType = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
): number => {
|
||||
const cursor = rowCursor({ bytes, layout, table: 2, row });
|
||||
cursor.readUInt32();
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.stringIndexSize);
|
||||
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
|
||||
cursor.readIndex(layout.tableIndexSize(4));
|
||||
return cursor.readIndex(layout.tableIndexSize(6));
|
||||
};
|
||||
|
||||
const attributeTypeName = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
rawType: number,
|
||||
maxBytes: number,
|
||||
): string | null => {
|
||||
const tag = rawType & 7;
|
||||
const row = Math.floor(rawType / 8);
|
||||
if (row === 0) return null;
|
||||
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, maxBytes);
|
||||
if (tag !== 3) return null;
|
||||
const memberRefs = layout.table(10);
|
||||
if (memberRefs === undefined || row > memberRefs.rowCount) return null;
|
||||
const cursor = rowCursor({ bytes, layout, table: 10, row });
|
||||
const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
|
||||
const parentTag = parent & 7;
|
||||
const parentRow = Math.floor(parent / 8);
|
||||
if (parentTag === 0)
|
||||
return readTypeName({ bytes, layout, table: 2, row: parentRow, maxBytes });
|
||||
if (parentTag === 1)
|
||||
return readTypeName({ bytes, layout, table: 1, row: parentRow, maxBytes });
|
||||
if (parentTag === 3)
|
||||
return declaringTypeForMethod(bytes, layout, parentRow, maxBytes);
|
||||
return null;
|
||||
};
|
||||
|
||||
const decodeCompressedLength = (
|
||||
blob: Buffer,
|
||||
offset: number,
|
||||
): { readonly length: number; readonly prefix: number } | undefined => {
|
||||
const first = blob[offset];
|
||||
if (first === undefined) return undefined;
|
||||
if ((first & 0x80) === 0) return { length: first, prefix: 1 };
|
||||
if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined)
|
||||
return {
|
||||
length: ((first & 0x3f) << 8) | (blob[offset + 1] ?? 0),
|
||||
prefix: 2,
|
||||
};
|
||||
if (
|
||||
(first & 0xe0) === 0xc0 &&
|
||||
blob[offset + 1] !== undefined &&
|
||||
blob[offset + 2] !== undefined &&
|
||||
blob[offset + 3] !== undefined
|
||||
)
|
||||
return {
|
||||
length:
|
||||
(first & 0x1f) * 0x01_00_00_00 +
|
||||
(blob[offset + 1] ?? 0) * 0x01_00_00 +
|
||||
(blob[offset + 2] ?? 0) * 0x0100 +
|
||||
(blob[offset + 3] ?? 0),
|
||||
prefix: 4,
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const decodeFixedString = (blob: Buffer): string | null => {
|
||||
if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff)
|
||||
return null;
|
||||
const length = decodeCompressedLength(blob, 2);
|
||||
if (length === undefined) return null;
|
||||
const start = 2 + length.prefix;
|
||||
if (start > blob.length - length.length) return null;
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
blob.subarray(start, start + length.length),
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const readCustomAttribute = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
row: number,
|
||||
maxBytes: number,
|
||||
): CustomAttribute => {
|
||||
const cursor = rowCursor({ bytes, layout, table: 12, row });
|
||||
const parentRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("HasCustomAttribute"),
|
||||
);
|
||||
const typeRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("CustomAttributeType"),
|
||||
);
|
||||
const value = readMetadataBlob(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const parent = codedToken(
|
||||
parentRaw,
|
||||
5,
|
||||
[
|
||||
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
|
||||
44, 43,
|
||||
],
|
||||
);
|
||||
if (parent === null)
|
||||
throw managedFailure(
|
||||
"invalid-row",
|
||||
"metadata.CustomAttribute",
|
||||
"CustomAttribute parent coded index is invalid",
|
||||
cursor.start,
|
||||
);
|
||||
const typeName = attributeTypeName(bytes, layout, typeRaw, maxBytes);
|
||||
return {
|
||||
parent_token: parent,
|
||||
constructor_token: codedToken(typeRaw, 3, [undefined, undefined, 6, 10]),
|
||||
type_name: typeName,
|
||||
value_length: value.length,
|
||||
value_sha256: sha256Bytes(value),
|
||||
decoded_fixed_string:
|
||||
typeName === "System.Runtime.Versioning.TargetFrameworkAttribute"
|
||||
? decodeFixedString(value)
|
||||
: null,
|
||||
token: metadataToken(12, row),
|
||||
row_offset: cursor.start,
|
||||
};
|
||||
};
|
||||
|
||||
interface ReadResourceContext {
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly row: number;
|
||||
readonly maxBytes: number;
|
||||
readonly directory: ManagedResourceDirectory | null;
|
||||
readonly issues: ManagedParseIssue[];
|
||||
}
|
||||
|
||||
export const readResource = ({
|
||||
bytes,
|
||||
layout,
|
||||
row,
|
||||
maxBytes,
|
||||
directory,
|
||||
issues,
|
||||
}: ReadResourceContext): ManagedResource => {
|
||||
const cursor = rowCursor({ bytes, layout, table: 40, row });
|
||||
const declaredOffset = cursor.readUInt32();
|
||||
const flags = cursor.readUInt32();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const implementationRaw = cursor.readIndex(
|
||||
layout.codedIndexSize("Implementation"),
|
||||
);
|
||||
const implementationToken = codedToken(implementationRaw, 2, [38, 35, 39]);
|
||||
let dataLength: number | null = null;
|
||||
let dataSha256: string | null = null;
|
||||
if (implementationRaw === 0) {
|
||||
if (directory === null || declaredOffset > directory.size - 4) {
|
||||
issues.push({
|
||||
code: "invalid-resource",
|
||||
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
|
||||
offset: directory?.offset ?? null,
|
||||
detail: "Embedded resource offset leaves the CLI resources directory",
|
||||
});
|
||||
} else {
|
||||
const start = directory.offset + declaredOffset;
|
||||
dataLength = bytes.readUInt32LE(start);
|
||||
if (dataLength > directory.size - declaredOffset - 4) {
|
||||
issues.push({
|
||||
code: "invalid-resource",
|
||||
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
|
||||
offset: start,
|
||||
detail: "Embedded resource length leaves the CLI resources directory",
|
||||
});
|
||||
} else if (dataLength > maxBytes) {
|
||||
issues.push({
|
||||
code: "limit-exceeded",
|
||||
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
|
||||
offset: start + 4,
|
||||
detail: `Embedded resource exceeds max_heap_item_bytes ${String(maxBytes)}; digest omitted`,
|
||||
});
|
||||
} else {
|
||||
dataSha256 = sha256Bytes(
|
||||
bytes.subarray(start + 4, start + 4 + dataLength),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name,
|
||||
flags,
|
||||
visibility:
|
||||
(flags & 7) === 1 ? "public" : (flags & 7) === 2 ? "private" : "unknown",
|
||||
implementation_token: implementationToken,
|
||||
embedded: implementationRaw === 0,
|
||||
declared_offset: declaredOffset,
|
||||
data_length: dataLength,
|
||||
data_sha256: dataSha256,
|
||||
token: metadataToken(40, row),
|
||||
row_offset: cursor.start,
|
||||
};
|
||||
};
|
||||
+116
-129
@@ -1,4 +1,8 @@
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
import {
|
||||
computeRowSize,
|
||||
type RowSizeContext,
|
||||
} from "./ManagedMetadataRowSizes.js";
|
||||
|
||||
export const METADATA_TABLE_NAMES = [
|
||||
"Module",
|
||||
@@ -80,7 +84,7 @@ export interface ManagedMetadataLayout {
|
||||
codedIndexSize(name: CodedIndexName): 2 | 4;
|
||||
}
|
||||
|
||||
type CodedIndexName = keyof typeof CODED_INDEXES;
|
||||
export type CodedIndexName = keyof typeof CODED_INDEXES;
|
||||
|
||||
const CODED_INDEXES = {
|
||||
TypeDefOrRef: { bits: 2, tables: [2, 1, 27] },
|
||||
@@ -196,129 +200,15 @@ const codedSize = (
|
||||
const tableSize = (index: number, rows: readonly number[]): 2 | 4 =>
|
||||
(rows[index] ?? 0) < 0x1_0000 ? 2 : 4;
|
||||
|
||||
const rowSize = (
|
||||
table: number,
|
||||
rowCounts: readonly number[],
|
||||
heaps: { readonly string: 2 | 4; readonly guid: 2 | 4; readonly blob: 2 | 4 },
|
||||
): number => {
|
||||
const t = (index: number): number => tableSize(index, rowCounts);
|
||||
const c = (name: CodedIndexName): number => codedSize(name, rowCounts);
|
||||
const { string: s, guid: g, blob: b } = heaps;
|
||||
switch (table) {
|
||||
case 0:
|
||||
return 2 + s + g * 3;
|
||||
case 1:
|
||||
return c("ResolutionScope") + s * 2;
|
||||
case 2:
|
||||
return 4 + s * 2 + c("TypeDefOrRef") + t(4) + t(6);
|
||||
case 3:
|
||||
return t(4);
|
||||
case 4:
|
||||
return 2 + s + b;
|
||||
case 5:
|
||||
return t(6);
|
||||
case 6:
|
||||
return 8 + s + b + t(8);
|
||||
case 7:
|
||||
return t(8);
|
||||
case 8:
|
||||
return 4 + s;
|
||||
case 9:
|
||||
return t(2) + c("TypeDefOrRef");
|
||||
case 10:
|
||||
return c("MemberRefParent") + s + b;
|
||||
case 11:
|
||||
return 2 + c("HasConstant") + b;
|
||||
case 12:
|
||||
return c("HasCustomAttribute") + c("CustomAttributeType") + b;
|
||||
case 13:
|
||||
return c("HasFieldMarshal") + b;
|
||||
case 14:
|
||||
return 2 + c("HasDeclSecurity") + b;
|
||||
case 15:
|
||||
return 6 + t(2);
|
||||
case 16:
|
||||
return 4 + t(4);
|
||||
case 17:
|
||||
return b;
|
||||
case 18:
|
||||
return t(2) + t(20);
|
||||
case 19:
|
||||
return t(20);
|
||||
case 20:
|
||||
return 2 + s + c("TypeDefOrRef");
|
||||
case 21:
|
||||
return t(2) + t(23);
|
||||
case 22:
|
||||
return t(23);
|
||||
case 23:
|
||||
return 2 + s + b;
|
||||
case 24:
|
||||
return 2 + t(6) + c("HasSemantics");
|
||||
case 25:
|
||||
return t(2) + c("MethodDefOrRef") * 2;
|
||||
case 26:
|
||||
return s;
|
||||
case 27:
|
||||
return b;
|
||||
case 28:
|
||||
return 2 + c("MemberForwarded") + s + t(26);
|
||||
case 29:
|
||||
return 4 + t(4);
|
||||
case 30:
|
||||
return 8;
|
||||
case 31:
|
||||
return 4;
|
||||
case 32:
|
||||
return 16 + b + s * 2;
|
||||
case 33:
|
||||
return 4;
|
||||
case 34:
|
||||
return 12;
|
||||
case 35:
|
||||
return 12 + b * 2 + s * 2;
|
||||
case 36:
|
||||
return 4 + t(35);
|
||||
case 37:
|
||||
return 12 + t(35);
|
||||
case 38:
|
||||
return 4 + s + b;
|
||||
case 39:
|
||||
return 8 + s * 2 + c("Implementation");
|
||||
case 40:
|
||||
return 8 + s + c("Implementation");
|
||||
case 41:
|
||||
return t(2) * 2;
|
||||
case 42:
|
||||
return 4 + c("TypeOrMethodDef") + s;
|
||||
case 43:
|
||||
return c("MethodDefOrRef") + b;
|
||||
case 44:
|
||||
return t(42) + c("TypeDefOrRef");
|
||||
default:
|
||||
throw managedFailure(
|
||||
"invalid-tables",
|
||||
"metadata.tables",
|
||||
`Metadata table ${String(table)} is unsupported`,
|
||||
);
|
||||
}
|
||||
};
|
||||
interface ParsedStreams {
|
||||
readonly streams: ReadonlyMap<string, MetadataStream>;
|
||||
readonly version: string;
|
||||
}
|
||||
|
||||
/** Parse ECMA-335 stream and table layout with checked byte extents. */
|
||||
export const readManagedMetadataLayout = (
|
||||
artifact: Buffer,
|
||||
const readMetadataStreams = (
|
||||
bytes: Buffer,
|
||||
rootOffset: number,
|
||||
size: number,
|
||||
maxTableRows: number,
|
||||
): ManagedMetadataLayout => {
|
||||
if (size < 20 || rootOffset < 0 || rootOffset > artifact.length - size)
|
||||
throw managedFailure(
|
||||
"invalid-metadata-root",
|
||||
"metadata.root",
|
||||
"CLI metadata directory leaves the artifact byte range",
|
||||
rootOffset,
|
||||
);
|
||||
const bytes = artifact.subarray(rootOffset, rootOffset + size);
|
||||
): ParsedStreams => {
|
||||
if (bytes.readUInt32LE(0) !== 0x424a_5342)
|
||||
throw managedFailure(
|
||||
"invalid-metadata-root",
|
||||
@@ -365,6 +255,26 @@ export const readManagedMetadataLayout = (
|
||||
});
|
||||
cursor = named.next;
|
||||
}
|
||||
return { streams, version };
|
||||
};
|
||||
|
||||
interface HeapSizes {
|
||||
readonly string: 2 | 4;
|
||||
readonly guid: 2 | 4;
|
||||
readonly blob: 2 | 4;
|
||||
}
|
||||
|
||||
interface ParsedTablesHeader {
|
||||
readonly tableBytes: Buffer;
|
||||
readonly tablesOffset: number;
|
||||
readonly heaps: HeapSizes;
|
||||
readonly valid: bigint;
|
||||
}
|
||||
|
||||
const readTablesHeader = (
|
||||
bytes: Buffer,
|
||||
streams: ReadonlyMap<string, MetadataStream>,
|
||||
): ParsedTablesHeader => {
|
||||
const tablesStream = streamByName(streams, ["#~", "#-"]);
|
||||
if (tablesStream === undefined)
|
||||
throw managedFailure(
|
||||
@@ -372,7 +282,7 @@ export const readManagedMetadataLayout = (
|
||||
"metadata.tables",
|
||||
"Required metadata table stream #~ or #- is absent",
|
||||
);
|
||||
const tableBytes = artifact.subarray(
|
||||
const tableBytes = bytes.subarray(
|
||||
tablesStream.offset,
|
||||
tablesStream.offset + tablesStream.size,
|
||||
);
|
||||
@@ -391,6 +301,15 @@ export const readManagedMetadataLayout = (
|
||||
"Metadata valid mask names unsupported tables outside the admitted ECMA-335 range",
|
||||
tablesStream.offset + 8,
|
||||
);
|
||||
return { tableBytes, tablesOffset: tablesStream.offset, heaps, valid };
|
||||
};
|
||||
|
||||
const readRowCounts = (
|
||||
tableBytes: Buffer,
|
||||
valid: bigint,
|
||||
maxTableRows: number,
|
||||
tablesOffset: number,
|
||||
): readonly number[] => {
|
||||
const rowCounts = Array<number>(METADATA_TABLE_NAMES.length).fill(0);
|
||||
let tableCursor = 24;
|
||||
for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) {
|
||||
@@ -402,23 +321,56 @@ export const readManagedMetadataLayout = (
|
||||
"limit-exceeded",
|
||||
`metadata.${METADATA_TABLE_NAMES[index] ?? String(index)}`,
|
||||
`Metadata table row count ${String(count)} exceeds max_table_rows ${String(maxTableRows)}`,
|
||||
tablesStream.offset + tableCursor,
|
||||
tablesOffset + tableCursor,
|
||||
);
|
||||
rowCounts[index] = count;
|
||||
tableCursor += 4;
|
||||
}
|
||||
return rowCounts;
|
||||
};
|
||||
|
||||
const buildRowSizeContext = (
|
||||
rowCounts: readonly number[],
|
||||
heaps: HeapSizes,
|
||||
): RowSizeContext => ({
|
||||
tableSize: (index) => tableSize(index, rowCounts),
|
||||
codedSize: (name) => codedSize(name, rowCounts),
|
||||
stringSize: heaps.string,
|
||||
guidSize: heaps.guid,
|
||||
blobSize: heaps.blob,
|
||||
});
|
||||
|
||||
const buildMetadataTables = ({
|
||||
tableBytes,
|
||||
rowCounts,
|
||||
heaps,
|
||||
valid,
|
||||
tablesOffset,
|
||||
}: {
|
||||
readonly tableBytes: Buffer;
|
||||
readonly rowCounts: readonly number[];
|
||||
readonly heaps: HeapSizes;
|
||||
readonly valid: bigint;
|
||||
readonly tablesOffset: number;
|
||||
}): ReadonlyMap<number, MetadataTableLayout> => {
|
||||
const tables = new Map<number, MetadataTableLayout>();
|
||||
const context = buildRowSizeContext(rowCounts, heaps);
|
||||
let tableCursor = 24;
|
||||
for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) {
|
||||
if ((valid & (1n << BigInt(index))) === 0n) continue;
|
||||
tableCursor += 4;
|
||||
}
|
||||
for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) {
|
||||
const count = rowCounts[index] ?? 0;
|
||||
if ((valid & (1n << BigInt(index))) === 0n) continue;
|
||||
const sizeOfRow = rowSize(index, rowCounts, heaps);
|
||||
const sizeOfRow = computeRowSize(index, context);
|
||||
const byteLength = count * sizeOfRow;
|
||||
if (!Number.isSafeInteger(byteLength))
|
||||
throw managedFailure(
|
||||
"invalid-tables",
|
||||
"metadata.tables",
|
||||
"Metadata table byte size overflowed",
|
||||
tablesStream.offset + tableCursor,
|
||||
tablesOffset + tableCursor,
|
||||
);
|
||||
requireRange(tableBytes, tableCursor, byteLength, "metadata.table-data");
|
||||
tables.set(index, {
|
||||
@@ -426,11 +378,47 @@ export const readManagedMetadataLayout = (
|
||||
name: METADATA_TABLE_NAMES[index] ?? `Table${String(index)}`,
|
||||
rowCount: count,
|
||||
rowSize: sizeOfRow,
|
||||
offset: tablesStream.offset + tableCursor,
|
||||
offset: tablesOffset + tableCursor,
|
||||
});
|
||||
tableCursor += byteLength;
|
||||
}
|
||||
const layout: ManagedMetadataLayout = {
|
||||
return tables;
|
||||
};
|
||||
|
||||
/** Parse ECMA-335 stream and table layout with checked byte extents. */
|
||||
export const readManagedMetadataLayout = (
|
||||
artifact: Buffer,
|
||||
rootOffset: number,
|
||||
size: number,
|
||||
maxTableRows: number,
|
||||
): ManagedMetadataLayout => {
|
||||
if (size < 20 || rootOffset < 0 || rootOffset > artifact.length - size)
|
||||
throw managedFailure(
|
||||
"invalid-metadata-root",
|
||||
"metadata.root",
|
||||
"CLI metadata directory leaves the artifact byte range",
|
||||
rootOffset,
|
||||
);
|
||||
const bytes = artifact.subarray(rootOffset, rootOffset + size);
|
||||
const { streams, version } = readMetadataStreams(bytes, rootOffset);
|
||||
const { tableBytes, tablesOffset, heaps, valid } = readTablesHeader(
|
||||
artifact,
|
||||
streams,
|
||||
);
|
||||
const rowCounts = readRowCounts(
|
||||
tableBytes,
|
||||
valid,
|
||||
maxTableRows,
|
||||
tablesOffset,
|
||||
);
|
||||
const tables = buildMetadataTables({
|
||||
tableBytes,
|
||||
rowCounts,
|
||||
heaps,
|
||||
valid,
|
||||
tablesOffset,
|
||||
});
|
||||
return {
|
||||
rootOffset,
|
||||
size,
|
||||
version,
|
||||
@@ -447,7 +435,6 @@ export const readManagedMetadataLayout = (
|
||||
tableIndexSize: (index) => tableSize(index, rowCounts),
|
||||
codedIndexSize: (name) => codedSize(name, rowCounts),
|
||||
};
|
||||
return layout;
|
||||
};
|
||||
|
||||
/** Resolve a one-based metadata row to its exact file offset. */
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
import type { CodedIndexName } from "./ManagedMetadataLayout.js";
|
||||
|
||||
export interface RowSizeContext {
|
||||
readonly tableSize: (index: number) => number;
|
||||
readonly codedSize: (name: CodedIndexName) => number;
|
||||
readonly stringSize: 2 | 4;
|
||||
readonly guidSize: 2 | 4;
|
||||
readonly blobSize: 2 | 4;
|
||||
}
|
||||
|
||||
type RowSizeFactory = (context: RowSizeContext) => number;
|
||||
|
||||
const ROW_SIZE_FACTORIES: readonly RowSizeFactory[] = [
|
||||
({ stringSize: s, guidSize: g }) => 2 + s + g * 3,
|
||||
({ codedSize: c, stringSize: s }) => c("ResolutionScope") + s * 2,
|
||||
({ tableSize: t, codedSize: c, stringSize: s }) =>
|
||||
4 + s * 2 + c("TypeDefOrRef") + t(4) + t(6),
|
||||
({ tableSize: t }) => t(4),
|
||||
({ stringSize: s, blobSize: b }) => 2 + s + b,
|
||||
({ tableSize: t }) => t(6),
|
||||
({ tableSize: t, stringSize: s, blobSize: b }) => 8 + s + b + t(8),
|
||||
({ tableSize: t }) => t(8),
|
||||
({ stringSize: s }) => 4 + s,
|
||||
({ tableSize: t, codedSize: c }) => t(2) + c("TypeDefOrRef"),
|
||||
({ codedSize: c, stringSize: s, blobSize: b }) =>
|
||||
c("MemberRefParent") + s + b,
|
||||
({ codedSize: c, blobSize: b }) => 2 + c("HasConstant") + b,
|
||||
({ codedSize: c, blobSize: b }) =>
|
||||
c("HasCustomAttribute") + c("CustomAttributeType") + b,
|
||||
({ codedSize: c, blobSize: b }) => c("HasFieldMarshal") + b,
|
||||
({ codedSize: c, blobSize: b }) => 2 + c("HasDeclSecurity") + b,
|
||||
({ tableSize: t }) => 6 + t(2),
|
||||
({ tableSize: t }) => 4 + t(4),
|
||||
({ blobSize: b }) => b,
|
||||
({ tableSize: t }) => t(2) + t(20),
|
||||
({ tableSize: t }) => t(20),
|
||||
({ stringSize: s, codedSize: c }) => 2 + s + c("TypeDefOrRef"),
|
||||
({ tableSize: t }) => t(2) + t(23),
|
||||
({ tableSize: t }) => t(23),
|
||||
({ stringSize: s, blobSize: b }) => 2 + s + b,
|
||||
({ tableSize: t, codedSize: c }) => 2 + t(6) + c("HasSemantics"),
|
||||
({ tableSize: t, codedSize: c }) => t(2) + c("MethodDefOrRef") * 2,
|
||||
({ stringSize: s }) => s,
|
||||
({ blobSize: b }) => b,
|
||||
({ tableSize: t, codedSize: c, stringSize: s }) =>
|
||||
2 + c("MemberForwarded") + s + t(26),
|
||||
({ tableSize: t }) => 4 + t(4),
|
||||
() => 8,
|
||||
() => 4,
|
||||
({ blobSize: b, stringSize: s }) => 16 + b + s * 2,
|
||||
() => 4,
|
||||
() => 12,
|
||||
({ blobSize: b, stringSize: s }) => 12 + b * 2 + s * 2,
|
||||
({ tableSize: t }) => 4 + t(35),
|
||||
({ tableSize: t }) => 12 + t(35),
|
||||
({ stringSize: s, blobSize: b }) => 4 + s + b,
|
||||
({ stringSize: s, codedSize: c }) => 8 + s * 2 + c("Implementation"),
|
||||
({ stringSize: s, codedSize: c }) => 8 + s + c("Implementation"),
|
||||
({ tableSize: t }) => t(2) * 2,
|
||||
({ stringSize: s, codedSize: c }) => 4 + c("TypeOrMethodDef") + s,
|
||||
({ codedSize: c, blobSize: b }) => c("MethodDefOrRef") + b,
|
||||
({ tableSize: t, codedSize: c }) => t(42) + c("TypeDefOrRef"),
|
||||
];
|
||||
|
||||
export const computeRowSize = (
|
||||
table: number,
|
||||
context: RowSizeContext,
|
||||
): number => {
|
||||
const factory = ROW_SIZE_FACTORIES[table];
|
||||
if (factory === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-tables",
|
||||
"metadata.tables",
|
||||
`Metadata table ${String(table)} is unsupported`,
|
||||
);
|
||||
return factory(context);
|
||||
};
|
||||
@@ -0,0 +1,499 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import {
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
type ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
import {
|
||||
metadataRowOffset,
|
||||
type ManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
MetadataRowCursor,
|
||||
metadataToken,
|
||||
readMetadataString,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import { managedTableRowCounts } from "./ManagedMetadataInventory.js";
|
||||
import type { ManagedMetadataInventory } from "./ManagedMetadataInventory.js";
|
||||
import { managedFailure } from "./ManagedReaderFailure.js";
|
||||
import type { ManagedNativeBoundaryInspectionLimits } from "./ManagedNativeBoundaryInspector.js";
|
||||
import type { ManagedPeLayout } from "./ManagedPeReader.js";
|
||||
|
||||
type Page<Item> = {
|
||||
readonly items: readonly Item[];
|
||||
readonly offset: number;
|
||||
readonly limit: number;
|
||||
readonly total: number;
|
||||
readonly returned: number;
|
||||
readonly dropped: number;
|
||||
readonly complete: boolean;
|
||||
};
|
||||
|
||||
type ModuleRef =
|
||||
ManagedNativeBoundaryInspection["module_refs"]["items"][number];
|
||||
type NativeImport =
|
||||
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
|
||||
type NativeImplementation =
|
||||
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
|
||||
|
||||
interface MemberCore {
|
||||
readonly token: string;
|
||||
readonly rowOffset: number;
|
||||
readonly kind: NativeImport["member_kind"];
|
||||
readonly name: string;
|
||||
readonly flags: number;
|
||||
readonly implFlags: number | null;
|
||||
readonly rva: number | null;
|
||||
}
|
||||
|
||||
interface RowCursorContext {
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly table: number;
|
||||
readonly row: number;
|
||||
}
|
||||
|
||||
const rowCursor = ({
|
||||
bytes,
|
||||
layout,
|
||||
table,
|
||||
row,
|
||||
}: RowCursorContext): MetadataRowCursor => {
|
||||
const descriptor = layout.table(table);
|
||||
const start = metadataRowOffset(layout, table, row);
|
||||
if (descriptor === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-row",
|
||||
`metadata.${String(table)}`,
|
||||
"Metadata table is absent",
|
||||
start,
|
||||
);
|
||||
return new MetadataRowCursor(
|
||||
bytes,
|
||||
start,
|
||||
start + descriptor.rowSize,
|
||||
`metadata.${descriptor.name}`,
|
||||
);
|
||||
};
|
||||
|
||||
const codedToken = (
|
||||
raw: number,
|
||||
tagBits: number,
|
||||
tables: readonly number[],
|
||||
): string | null => {
|
||||
const tagMask = (1 << tagBits) - 1;
|
||||
const table = tables[raw & tagMask];
|
||||
const row = raw >> tagBits;
|
||||
if (table === undefined || row === 0) return null;
|
||||
return metadataToken(table, row);
|
||||
};
|
||||
|
||||
const flagsHex = (value: number): string =>
|
||||
`0x${value.toString(16).padStart(4, "0")}`;
|
||||
|
||||
const page = <Item>(
|
||||
items: readonly Item[],
|
||||
offset: number,
|
||||
limit: number,
|
||||
): Page<Item> => {
|
||||
const safeOffset = Math.min(offset, items.length);
|
||||
const selected = items.slice(safeOffset, safeOffset + limit);
|
||||
return {
|
||||
items: selected,
|
||||
offset,
|
||||
limit,
|
||||
total: items.length,
|
||||
returned: selected.length,
|
||||
dropped: Math.max(0, items.length - safeOffset - selected.length),
|
||||
complete: safeOffset === 0 && selected.length === items.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseModuleRefs = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): readonly ModuleRef[] => {
|
||||
const refs: ModuleRef[] = [];
|
||||
const table = layout.table(26);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor({ bytes, layout, table: 26, row });
|
||||
refs.push({
|
||||
token: metadataToken(26, row),
|
||||
row_offset: cursor.start,
|
||||
name: readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
),
|
||||
});
|
||||
}
|
||||
return refs;
|
||||
};
|
||||
|
||||
export const parseFields = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): ReadonlyMap<string, MemberCore> => {
|
||||
const fields = new Map<string, MemberCore>();
|
||||
const table = layout.table(4);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor({ bytes, layout, table: 4, row });
|
||||
const flags = cursor.readUInt16();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
cursor.readIndex(layout.blobIndexSize);
|
||||
const token = metadataToken(4, row);
|
||||
fields.set(token, {
|
||||
token,
|
||||
rowOffset: cursor.start,
|
||||
kind: "field",
|
||||
name,
|
||||
flags,
|
||||
implFlags: null,
|
||||
rva: null,
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
export const parseMethods = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): ReadonlyMap<string, MemberCore> => {
|
||||
const methods = new Map<string, MemberCore>();
|
||||
const table = layout.table(6);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor({ bytes, layout, table: 6, row });
|
||||
const rva = cursor.readUInt32();
|
||||
const implFlags = cursor.readUInt16();
|
||||
const flags = cursor.readUInt16();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
cursor.readIndex(layout.blobIndexSize);
|
||||
cursor.readIndex(layout.tableIndexSize(8));
|
||||
const token = metadataToken(6, row);
|
||||
methods.set(token, {
|
||||
token,
|
||||
rowOffset: cursor.start,
|
||||
kind: "method",
|
||||
name,
|
||||
flags,
|
||||
implFlags,
|
||||
rva,
|
||||
});
|
||||
}
|
||||
return methods;
|
||||
};
|
||||
|
||||
interface ParseImplMapsContext {
|
||||
readonly bytes: Buffer;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly maxBytes: number;
|
||||
readonly modules: readonly ModuleRef[];
|
||||
readonly members: ReadonlyMap<string, MemberCore>;
|
||||
}
|
||||
|
||||
export const parseImplMaps = ({
|
||||
bytes,
|
||||
layout,
|
||||
maxBytes,
|
||||
modules,
|
||||
members,
|
||||
}: ParseImplMapsContext): readonly NativeImport[] => {
|
||||
const moduleNames = new Map(modules.map((module) => [module.token, module]));
|
||||
const imports: NativeImport[] = [];
|
||||
const table = layout.table(28);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor({ bytes, layout, table: 28, row });
|
||||
const mappingFlags = cursor.readUInt16();
|
||||
const memberToken = codedToken(
|
||||
cursor.readIndex(layout.codedIndexSize("MemberForwarded")),
|
||||
1,
|
||||
[4, 6],
|
||||
);
|
||||
const importName = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const importScopeRow = cursor.readIndex(layout.tableIndexSize(26));
|
||||
const importScopeToken =
|
||||
importScopeRow === 0 ? null : metadataToken(26, importScopeRow);
|
||||
const member = memberToken === null ? undefined : members.get(memberToken);
|
||||
imports.push({
|
||||
token: metadataToken(28, row),
|
||||
row_offset: cursor.start,
|
||||
mapping_flags: mappingFlags,
|
||||
mapping_flags_hex: flagsHex(mappingFlags),
|
||||
member_token: memberToken,
|
||||
member_kind: member?.kind ?? "unknown",
|
||||
member_name: member?.name ?? null,
|
||||
import_name: importName,
|
||||
import_scope_token: importScopeToken,
|
||||
import_scope_name:
|
||||
importScopeToken === null
|
||||
? null
|
||||
: (moduleNames.get(importScopeToken)?.name ?? null),
|
||||
no_mangle: (mappingFlags & 0x0001) !== 0,
|
||||
char_set: charSet(mappingFlags),
|
||||
call_convention: callConvention(mappingFlags),
|
||||
supports_last_error: (mappingFlags & 0x0040) !== 0,
|
||||
best_fit: bestFit(mappingFlags),
|
||||
throw_on_unmappable_char: throwOnUnmappable(mappingFlags),
|
||||
verification: "managed-declaration-only",
|
||||
});
|
||||
}
|
||||
return imports;
|
||||
};
|
||||
|
||||
const charSet = (flags: number): NativeImport["char_set"] => {
|
||||
switch (flags & 0x0006) {
|
||||
case 0x0000:
|
||||
return "not-specified";
|
||||
case 0x0002:
|
||||
return "ansi";
|
||||
case 0x0004:
|
||||
return "unicode";
|
||||
case 0x0006:
|
||||
return "auto";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const callConvention = (flags: number): NativeImport["call_convention"] => {
|
||||
switch (flags & 0x0700) {
|
||||
case 0x0000:
|
||||
return "not-specified";
|
||||
case 0x0100:
|
||||
return "winapi";
|
||||
case 0x0200:
|
||||
return "cdecl";
|
||||
case 0x0300:
|
||||
return "stdcall";
|
||||
case 0x0400:
|
||||
return "thiscall";
|
||||
case 0x0500:
|
||||
return "fastcall";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const bestFit = (flags: number): NativeImport["best_fit"] => {
|
||||
switch (flags & 0x0030) {
|
||||
case 0x0000:
|
||||
return "assembly-default";
|
||||
case 0x0010:
|
||||
return "enabled";
|
||||
case 0x0020:
|
||||
return "disabled";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const throwOnUnmappable = (
|
||||
flags: number,
|
||||
): NativeImport["throw_on_unmappable_char"] => {
|
||||
switch (flags & 0x3000) {
|
||||
case 0x0000:
|
||||
return "assembly-default";
|
||||
case 0x1000:
|
||||
return "enabled";
|
||||
case 0x2000:
|
||||
return "disabled";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
export const nativeImplementations = (
|
||||
methods: Iterable<MemberCore>,
|
||||
pinvokeTokens: ReadonlySet<string>,
|
||||
): readonly NativeImplementation[] => {
|
||||
const implementations: NativeImplementation[] = [];
|
||||
for (const method of methods) {
|
||||
const implFlags = method.implFlags ?? 0;
|
||||
const codeType = codeTypeFor(implFlags);
|
||||
const managedKind = managedKindFor(implFlags);
|
||||
const pinvokeDeclared =
|
||||
pinvokeTokens.has(method.token) || (method.flags & 0x2000) !== 0;
|
||||
if (!pinvokeDeclared && codeType === "il" && managedKind === "managed")
|
||||
continue;
|
||||
implementations.push({
|
||||
token: method.token,
|
||||
row_offset: method.rowOffset,
|
||||
name: method.name,
|
||||
rva: method.rva ?? 0,
|
||||
flags: method.flags,
|
||||
impl_flags: implFlags,
|
||||
code_type: codeType,
|
||||
managed_kind: managedKind,
|
||||
pinvoke_declared: pinvokeDeclared,
|
||||
boundary_kind: boundaryKind(pinvokeDeclared, codeType, managedKind),
|
||||
body_interpretation:
|
||||
codeType === "il" && managedKind === "managed" && !pinvokeDeclared
|
||||
? "managed-cil"
|
||||
: method.rva === 0
|
||||
? "not-file-backed"
|
||||
: "native-or-runtime",
|
||||
});
|
||||
}
|
||||
return implementations;
|
||||
};
|
||||
|
||||
const codeTypeFor = (implFlags: number): NativeImplementation["code_type"] => {
|
||||
switch (implFlags & 0x0003) {
|
||||
case 0x0000:
|
||||
return "il";
|
||||
case 0x0001:
|
||||
return "native";
|
||||
case 0x0002:
|
||||
return "optil";
|
||||
case 0x0003:
|
||||
return "runtime";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const managedKindFor = (
|
||||
implFlags: number,
|
||||
): NativeImplementation["managed_kind"] => {
|
||||
switch (implFlags & 0x0004) {
|
||||
case 0x0000:
|
||||
return "managed";
|
||||
case 0x0004:
|
||||
return "unmanaged";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const boundaryKind = (
|
||||
pinvokeDeclared: boolean,
|
||||
codeType: NativeImplementation["code_type"],
|
||||
managedKind: NativeImplementation["managed_kind"],
|
||||
): NativeImplementation["boundary_kind"] => {
|
||||
if (pinvokeDeclared) return "pinvoke";
|
||||
if (codeType === "native") return "native-body";
|
||||
if (codeType === "runtime") return "runtime-provided";
|
||||
if (managedKind === "unmanaged") return "unmanaged-method";
|
||||
return "mixed-or-unknown";
|
||||
};
|
||||
|
||||
export const cliNative = (
|
||||
pe: ManagedPeLayout,
|
||||
): ManagedNativeBoundaryInspection["cli_native"] => {
|
||||
if (pe.cli === null)
|
||||
return {
|
||||
il_only: false,
|
||||
requires_32bit: false,
|
||||
strong_name_signed: false,
|
||||
native_entry_point: false,
|
||||
ready_to_run_signature: false,
|
||||
managed_native_header_rva: 0,
|
||||
managed_native_header_size: 0,
|
||||
};
|
||||
return {
|
||||
il_only: (pe.cli.flags & 0x0000_0001) !== 0,
|
||||
requires_32bit: (pe.cli.flags & 0x0000_0002) !== 0,
|
||||
strong_name_signed: (pe.cli.flags & 0x0000_0008) !== 0,
|
||||
native_entry_point: (pe.cli.flags & 0x0000_0010) !== 0,
|
||||
ready_to_run_signature: pe.cli.readyToRunSignature,
|
||||
managed_native_header_rva: pe.cli.managedNativeHeader.rva,
|
||||
managed_native_header_size: pe.cli.managedNativeHeader.size,
|
||||
};
|
||||
};
|
||||
|
||||
interface BoundaryInspectionContext {
|
||||
readonly target: BinaryTarget;
|
||||
readonly bytes: Buffer;
|
||||
readonly limits: ManagedNativeBoundaryInspectionLimits;
|
||||
readonly pe: ManagedPeLayout;
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly inventory: ManagedMetadataInventory;
|
||||
readonly moduleRefs: readonly ModuleRef[];
|
||||
readonly imports: readonly NativeImport[];
|
||||
readonly implementations: readonly NativeImplementation[];
|
||||
readonly native: ManagedNativeBoundaryInspection["cli_native"];
|
||||
readonly issues: readonly ManagedParseIssue[];
|
||||
}
|
||||
|
||||
export const buildNativeBoundaryInspection = ({
|
||||
target,
|
||||
bytes,
|
||||
limits,
|
||||
layout,
|
||||
inventory,
|
||||
moduleRefs,
|
||||
imports,
|
||||
implementations,
|
||||
native,
|
||||
issues,
|
||||
}: BoundaryInspectionContext): ManagedNativeBoundaryInspection =>
|
||||
managedNativeBoundaryInspectionSchema.parse({
|
||||
schema_version: 1,
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: inventory.module,
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: layout.version,
|
||||
table_row_counts: managedTableRowCounts(layout),
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
cli_native: native,
|
||||
module_refs: page(
|
||||
moduleRefs,
|
||||
limits.moduleRefOffset,
|
||||
limits.moduleRefLimit,
|
||||
),
|
||||
pinvoke_imports: page(imports, limits.importOffset, limits.importLimit),
|
||||
native_implementations: page(
|
||||
implementations,
|
||||
limits.implementationOffset,
|
||||
limits.implementationLimit,
|
||||
),
|
||||
summary: {
|
||||
module_ref_count: moduleRefs.length,
|
||||
pinvoke_import_count: imports.length,
|
||||
native_implementation_count: implementations.length,
|
||||
ready_to_run: native.ready_to_run_signature,
|
||||
mixed_mode_or_native_header:
|
||||
native.managed_native_header_rva !== 0 ||
|
||||
native.managed_native_header_size !== 0 ||
|
||||
native.native_entry_point,
|
||||
},
|
||||
coverage: {
|
||||
state: issues.length === 0 ? "complete" : "partial",
|
||||
issues,
|
||||
},
|
||||
limitations: [
|
||||
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
|
||||
"Managed metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
|
||||
"ReadyToRun, NativeAOT, C++/CLI, and IL2CPP native semantics require separately selected native-provider evidence; this tool does not translate managed tokens into native addresses.",
|
||||
],
|
||||
});
|
||||
@@ -4,30 +4,29 @@ import {
|
||||
type ManagedNativeBoundaryInspection,
|
||||
type ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
import {
|
||||
metadataRowOffset,
|
||||
type ManagedMetadataLayout,
|
||||
readManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
MetadataRowCursor,
|
||||
metadataToken,
|
||||
readMetadataString,
|
||||
} from "./ManagedMetadataHeaps.js";
|
||||
import {
|
||||
readManagedMetadataInventory,
|
||||
managedTableRowCounts,
|
||||
type ManagedResourceDirectory,
|
||||
} from "./ManagedMetadataInventory.js";
|
||||
import {
|
||||
readManagedMetadataLayout,
|
||||
type ManagedMetadataLayout,
|
||||
} from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
readManagedPeLayout,
|
||||
type ManagedPeLayout,
|
||||
} from "./ManagedPeReader.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import {
|
||||
ManagedReaderFailure,
|
||||
managedFailure,
|
||||
} from "./ManagedReaderFailure.js";
|
||||
buildNativeBoundaryInspection,
|
||||
cliNative,
|
||||
nativeImplementations,
|
||||
parseFields,
|
||||
parseImplMaps,
|
||||
parseMethods,
|
||||
parseModuleRefs,
|
||||
} from "./ManagedNativeBoundaryHelpers.js";
|
||||
|
||||
/** Bounds for execution-free managed/native boundary inspection. */
|
||||
export interface ManagedNativeBoundaryInspectionLimits {
|
||||
readonly moduleRefOffset: number;
|
||||
readonly moduleRefLimit: number;
|
||||
@@ -40,396 +39,25 @@ export interface ManagedNativeBoundaryInspectionLimits {
|
||||
readonly maxHeapItemBytes: number;
|
||||
}
|
||||
|
||||
type Page<Item> = {
|
||||
readonly items: readonly Item[];
|
||||
readonly offset: number;
|
||||
readonly limit: number;
|
||||
readonly total: number;
|
||||
readonly returned: number;
|
||||
readonly dropped: number;
|
||||
readonly complete: boolean;
|
||||
};
|
||||
type ModuleRef =
|
||||
ManagedNativeBoundaryInspection["module_refs"]["items"][number];
|
||||
type NativeImport =
|
||||
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
|
||||
type NativeImplementation =
|
||||
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
|
||||
type Inventory = ReturnType<typeof readManagedMetadataInventory>;
|
||||
|
||||
interface MemberCore {
|
||||
readonly token: string;
|
||||
readonly rowOffset: number;
|
||||
readonly kind: NativeImport["member_kind"];
|
||||
readonly name: string;
|
||||
readonly flags: number;
|
||||
readonly implFlags: number | null;
|
||||
readonly rva: number | null;
|
||||
}
|
||||
const emptyPage = <Item>(): ManagedNativeBoundaryInspection["module_refs"] & {
|
||||
items: readonly Item[];
|
||||
} => ({
|
||||
items: [],
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
total: 0,
|
||||
returned: 0,
|
||||
dropped: 0,
|
||||
complete: true,
|
||||
});
|
||||
|
||||
const rowCursor = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
table: number,
|
||||
row: number,
|
||||
): MetadataRowCursor => {
|
||||
const descriptor = layout.table(table);
|
||||
const start = metadataRowOffset(layout, table, row);
|
||||
if (descriptor === undefined)
|
||||
throw managedFailure(
|
||||
"invalid-row",
|
||||
`metadata.${String(table)}`,
|
||||
"Metadata table is absent",
|
||||
start,
|
||||
);
|
||||
return new MetadataRowCursor(
|
||||
bytes,
|
||||
start,
|
||||
start + descriptor.rowSize,
|
||||
`metadata.${descriptor.name}`,
|
||||
);
|
||||
};
|
||||
|
||||
const codedToken = (
|
||||
raw: number,
|
||||
tagBits: number,
|
||||
tables: readonly number[],
|
||||
): string | null => {
|
||||
const tagMask = (1 << tagBits) - 1;
|
||||
const table = tables[raw & tagMask];
|
||||
const row = raw >> tagBits;
|
||||
if (table === undefined || row === 0) return null;
|
||||
return metadataToken(table, row);
|
||||
};
|
||||
|
||||
const flagsHex = (value: number): string =>
|
||||
`0x${value.toString(16).padStart(4, "0")}`;
|
||||
|
||||
const page = <Item>(
|
||||
items: readonly Item[],
|
||||
offset: number,
|
||||
limit: number,
|
||||
): Page<Item> => {
|
||||
const safeOffset = Math.min(offset, items.length);
|
||||
const selected = items.slice(safeOffset, safeOffset + limit);
|
||||
return {
|
||||
items: selected,
|
||||
offset,
|
||||
limit,
|
||||
total: items.length,
|
||||
returned: selected.length,
|
||||
dropped: Math.max(0, items.length - safeOffset - selected.length),
|
||||
complete: safeOffset === 0 && selected.length === items.length,
|
||||
};
|
||||
};
|
||||
|
||||
const parseModuleRefs = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): readonly ModuleRef[] => {
|
||||
const refs: ModuleRef[] = [];
|
||||
const table = layout.table(26);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor(bytes, layout, 26, row);
|
||||
refs.push({
|
||||
token: metadataToken(26, row),
|
||||
row_offset: cursor.start,
|
||||
name: readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
),
|
||||
});
|
||||
}
|
||||
return refs;
|
||||
};
|
||||
|
||||
const parseFields = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): ReadonlyMap<string, MemberCore> => {
|
||||
const fields = new Map<string, MemberCore>();
|
||||
const table = layout.table(4);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor(bytes, layout, 4, row);
|
||||
const flags = cursor.readUInt16();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
cursor.readIndex(layout.blobIndexSize);
|
||||
const token = metadataToken(4, row);
|
||||
fields.set(token, {
|
||||
token,
|
||||
rowOffset: cursor.start,
|
||||
kind: "field",
|
||||
name,
|
||||
flags,
|
||||
implFlags: null,
|
||||
rva: null,
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
const parseMethods = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
): ReadonlyMap<string, MemberCore> => {
|
||||
const methods = new Map<string, MemberCore>();
|
||||
const table = layout.table(6);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor(bytes, layout, 6, row);
|
||||
const rva = cursor.readUInt32();
|
||||
const implFlags = cursor.readUInt16();
|
||||
const flags = cursor.readUInt16();
|
||||
const name = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
cursor.readIndex(layout.blobIndexSize);
|
||||
cursor.readIndex(layout.tableIndexSize(8));
|
||||
const token = metadataToken(6, row);
|
||||
methods.set(token, {
|
||||
token,
|
||||
rowOffset: cursor.start,
|
||||
kind: "method",
|
||||
name,
|
||||
flags,
|
||||
implFlags,
|
||||
rva,
|
||||
});
|
||||
}
|
||||
return methods;
|
||||
};
|
||||
|
||||
const parseImplMaps = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
maxBytes: number,
|
||||
modules: readonly ModuleRef[],
|
||||
members: ReadonlyMap<string, MemberCore>,
|
||||
): readonly NativeImport[] => {
|
||||
const moduleNames = new Map(modules.map((module) => [module.token, module]));
|
||||
const imports: NativeImport[] = [];
|
||||
const table = layout.table(28);
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = rowCursor(bytes, layout, 28, row);
|
||||
const mappingFlags = cursor.readUInt16();
|
||||
const memberToken = codedToken(
|
||||
cursor.readIndex(layout.codedIndexSize("MemberForwarded")),
|
||||
1,
|
||||
[4, 6],
|
||||
);
|
||||
const importName = readMetadataString(
|
||||
bytes,
|
||||
layout,
|
||||
cursor.readIndex(layout.stringIndexSize),
|
||||
maxBytes,
|
||||
);
|
||||
const importScopeRow = cursor.readIndex(layout.tableIndexSize(26));
|
||||
const importScopeToken =
|
||||
importScopeRow === 0 ? null : metadataToken(26, importScopeRow);
|
||||
const member = memberToken === null ? undefined : members.get(memberToken);
|
||||
imports.push({
|
||||
token: metadataToken(28, row),
|
||||
row_offset: cursor.start,
|
||||
mapping_flags: mappingFlags,
|
||||
mapping_flags_hex: flagsHex(mappingFlags),
|
||||
member_token: memberToken,
|
||||
member_kind: member?.kind ?? "unknown",
|
||||
member_name: member?.name ?? null,
|
||||
import_name: importName,
|
||||
import_scope_token: importScopeToken,
|
||||
import_scope_name:
|
||||
importScopeToken === null
|
||||
? null
|
||||
: (moduleNames.get(importScopeToken)?.name ?? null),
|
||||
no_mangle: (mappingFlags & 0x0001) !== 0,
|
||||
char_set: charSet(mappingFlags),
|
||||
call_convention: callConvention(mappingFlags),
|
||||
supports_last_error: (mappingFlags & 0x0040) !== 0,
|
||||
best_fit: bestFit(mappingFlags),
|
||||
throw_on_unmappable_char: throwOnUnmappable(mappingFlags),
|
||||
verification: "managed-declaration-only",
|
||||
});
|
||||
}
|
||||
return imports;
|
||||
};
|
||||
|
||||
const charSet = (flags: number): NativeImport["char_set"] => {
|
||||
switch (flags & 0x0006) {
|
||||
case 0x0000:
|
||||
return "not-specified";
|
||||
case 0x0002:
|
||||
return "ansi";
|
||||
case 0x0004:
|
||||
return "unicode";
|
||||
case 0x0006:
|
||||
return "auto";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const callConvention = (flags: number): NativeImport["call_convention"] => {
|
||||
switch (flags & 0x0700) {
|
||||
case 0x0000:
|
||||
return "not-specified";
|
||||
case 0x0100:
|
||||
return "winapi";
|
||||
case 0x0200:
|
||||
return "cdecl";
|
||||
case 0x0300:
|
||||
return "stdcall";
|
||||
case 0x0400:
|
||||
return "thiscall";
|
||||
case 0x0500:
|
||||
return "fastcall";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const bestFit = (flags: number): NativeImport["best_fit"] => {
|
||||
switch (flags & 0x0030) {
|
||||
case 0x0000:
|
||||
return "assembly-default";
|
||||
case 0x0010:
|
||||
return "enabled";
|
||||
case 0x0020:
|
||||
return "disabled";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const throwOnUnmappable = (
|
||||
flags: number,
|
||||
): NativeImport["throw_on_unmappable_char"] => {
|
||||
switch (flags & 0x3000) {
|
||||
case 0x0000:
|
||||
return "assembly-default";
|
||||
case 0x1000:
|
||||
return "enabled";
|
||||
case 0x2000:
|
||||
return "disabled";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const nativeImplementations = (
|
||||
methods: Iterable<MemberCore>,
|
||||
pinvokeTokens: ReadonlySet<string>,
|
||||
): readonly NativeImplementation[] => {
|
||||
const implementations: NativeImplementation[] = [];
|
||||
for (const method of methods) {
|
||||
const implFlags = method.implFlags ?? 0;
|
||||
const codeType = codeTypeFor(implFlags);
|
||||
const managedKind = managedKindFor(implFlags);
|
||||
const pinvokeDeclared =
|
||||
pinvokeTokens.has(method.token) || (method.flags & 0x2000) !== 0;
|
||||
if (!pinvokeDeclared && codeType === "il" && managedKind === "managed")
|
||||
continue;
|
||||
implementations.push({
|
||||
token: method.token,
|
||||
row_offset: method.rowOffset,
|
||||
name: method.name,
|
||||
rva: method.rva ?? 0,
|
||||
flags: method.flags,
|
||||
impl_flags: implFlags,
|
||||
code_type: codeType,
|
||||
managed_kind: managedKind,
|
||||
pinvoke_declared: pinvokeDeclared,
|
||||
boundary_kind: boundaryKind(pinvokeDeclared, codeType, managedKind),
|
||||
body_interpretation:
|
||||
codeType === "il" && managedKind === "managed" && !pinvokeDeclared
|
||||
? "managed-cil"
|
||||
: method.rva === 0
|
||||
? "not-file-backed"
|
||||
: "native-or-runtime",
|
||||
});
|
||||
}
|
||||
return implementations;
|
||||
};
|
||||
|
||||
const codeTypeFor = (implFlags: number): NativeImplementation["code_type"] => {
|
||||
switch (implFlags & 0x0003) {
|
||||
case 0x0000:
|
||||
return "il";
|
||||
case 0x0001:
|
||||
return "native";
|
||||
case 0x0002:
|
||||
return "optil";
|
||||
case 0x0003:
|
||||
return "runtime";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const managedKindFor = (
|
||||
implFlags: number,
|
||||
): NativeImplementation["managed_kind"] => {
|
||||
switch (implFlags & 0x0004) {
|
||||
case 0x0000:
|
||||
return "managed";
|
||||
case 0x0004:
|
||||
return "unmanaged";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const boundaryKind = (
|
||||
pinvokeDeclared: boolean,
|
||||
codeType: NativeImplementation["code_type"],
|
||||
managedKind: NativeImplementation["managed_kind"],
|
||||
): NativeImplementation["boundary_kind"] => {
|
||||
if (pinvokeDeclared) return "pinvoke";
|
||||
if (codeType === "native") return "native-body";
|
||||
if (codeType === "runtime") return "runtime-provided";
|
||||
if (managedKind === "unmanaged") return "unmanaged-method";
|
||||
return "mixed-or-unknown";
|
||||
};
|
||||
|
||||
const cliNative = (
|
||||
pe: ManagedPeLayout,
|
||||
): ManagedNativeBoundaryInspection["cli_native"] => {
|
||||
if (pe.cli === null)
|
||||
return {
|
||||
il_only: false,
|
||||
requires_32bit: false,
|
||||
strong_name_signed: false,
|
||||
native_entry_point: false,
|
||||
ready_to_run_signature: false,
|
||||
managed_native_header_rva: 0,
|
||||
managed_native_header_size: 0,
|
||||
};
|
||||
return {
|
||||
il_only: (pe.cli.flags & 0x0000_0001) !== 0,
|
||||
requires_32bit: (pe.cli.flags & 0x0000_0002) !== 0,
|
||||
strong_name_signed: (pe.cli.flags & 0x0000_0008) !== 0,
|
||||
native_entry_point: (pe.cli.flags & 0x0000_0010) !== 0,
|
||||
ready_to_run_signature: pe.cli.readyToRunSignature,
|
||||
managed_native_header_rva: pe.cli.managedNativeHeader.rva,
|
||||
managed_native_header_size: pe.cli.managedNativeHeader.size,
|
||||
};
|
||||
};
|
||||
|
||||
const unavailable = (
|
||||
const emptyInspection = (
|
||||
target: BinaryTarget,
|
||||
bytes: Buffer,
|
||||
limits: ManagedNativeBoundaryInspectionLimits,
|
||||
issue: ManagedParseIssue | null,
|
||||
classification: "not-managed" | "malformed" | "not-available" | "managed",
|
||||
issues: readonly ManagedParseIssue[] = [],
|
||||
): ManagedNativeBoundaryInspection =>
|
||||
managedNativeBoundaryInspectionSchema.parse({
|
||||
schema_version: 1,
|
||||
@@ -440,7 +68,14 @@ const unavailable = (
|
||||
format: "pe",
|
||||
},
|
||||
module: null,
|
||||
metadata: { status: "absent", version: null, table_row_counts: {} },
|
||||
metadata: {
|
||||
status:
|
||||
classification === "malformed" || classification === "not-available"
|
||||
? "malformed"
|
||||
: "absent",
|
||||
version: null,
|
||||
table_row_counts: {},
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
@@ -455,13 +90,9 @@ const unavailable = (
|
||||
managed_native_header_rva: 0,
|
||||
managed_native_header_size: 0,
|
||||
},
|
||||
module_refs: page([], limits.moduleRefOffset, limits.moduleRefLimit),
|
||||
pinvoke_imports: page([], limits.importOffset, limits.importLimit),
|
||||
native_implementations: page(
|
||||
[],
|
||||
limits.implementationOffset,
|
||||
limits.implementationLimit,
|
||||
),
|
||||
module_refs: emptyPage(),
|
||||
pinvoke_imports: emptyPage(),
|
||||
native_implementations: emptyPage(),
|
||||
summary: {
|
||||
module_ref_count: 0,
|
||||
pinvoke_import_count: 0,
|
||||
@@ -469,132 +100,132 @@ const unavailable = (
|
||||
ready_to_run: false,
|
||||
mixed_mode_or_native_header: false,
|
||||
},
|
||||
coverage: {
|
||||
state: "unavailable",
|
||||
issues: issue === null ? [] : [issue],
|
||||
},
|
||||
coverage: { state: classification, issues },
|
||||
limitations: [
|
||||
"The PE has no admitted CLI metadata; managed/native boundary inspection is unavailable.",
|
||||
"No CLI data was admitted; native boundary declarations are unavailable.",
|
||||
"Static inspection does not load or execute target code, so native export resolution is not performed.",
|
||||
],
|
||||
});
|
||||
|
||||
/** Inspect PE/CLI managed/native boundary declarations without executing code. */
|
||||
const readBoundaryInventory = (
|
||||
bytes: Buffer,
|
||||
pe: ManagedPeLayout,
|
||||
limits: ManagedNativeBoundaryInspectionLimits,
|
||||
): {
|
||||
readonly layout: ManagedMetadataLayout;
|
||||
readonly inventory: Inventory;
|
||||
} => {
|
||||
const cli = pe.cli!;
|
||||
const metadataOffset = pe.rvaToOffset(
|
||||
cli.metadata.rva,
|
||||
cli.metadata.size,
|
||||
"cli.metadata",
|
||||
);
|
||||
const layout = readManagedMetadataLayout(
|
||||
bytes,
|
||||
metadataOffset,
|
||||
cli.metadata.size,
|
||||
limits.maxTableRows,
|
||||
);
|
||||
const resourceDirectory: ManagedResourceDirectory = {
|
||||
offset: pe.rvaToOffset(
|
||||
cli.resources.rva,
|
||||
cli.resources.size,
|
||||
"cli.resources",
|
||||
),
|
||||
size: cli.resources.size,
|
||||
};
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
layout,
|
||||
{
|
||||
referenceOffset: 0,
|
||||
referenceLimit: 0,
|
||||
resourceOffset: 0,
|
||||
resourceLimit: 0,
|
||||
attributeOffset: 0,
|
||||
attributeLimit: 0,
|
||||
maxHeapItemBytes: limits.maxHeapItemBytes,
|
||||
},
|
||||
resourceDirectory,
|
||||
);
|
||||
return { layout, inventory };
|
||||
};
|
||||
|
||||
const metadataLimitIssue = (
|
||||
cli: NonNullable<ManagedPeLayout["cli"]>,
|
||||
maxBytes: number,
|
||||
): ManagedParseIssue => ({
|
||||
code: "limit-exceeded",
|
||||
scope: "cli.metadata",
|
||||
offset: cli.headerOffset + 8,
|
||||
detail: `CLI metadata size exceeds max_metadata_bytes ${String(maxBytes)}`,
|
||||
});
|
||||
|
||||
/** Inspect managed/native boundary declarations from PE metadata without execution. */
|
||||
export const inspectManagedNativeBoundariesBytes = (
|
||||
bytes: Buffer,
|
||||
target: BinaryTarget,
|
||||
limits: ManagedNativeBoundaryInspectionLimits,
|
||||
): ManagedNativeBoundaryInspection => {
|
||||
const pe = readManagedPeLayout(bytes);
|
||||
if (pe.cli === null) return unavailable(target, bytes, limits, pe.cliIssue);
|
||||
const issues: ManagedParseIssue[] = [];
|
||||
let pe: ManagedPeLayout;
|
||||
try {
|
||||
const rootOffset = pe.rvaToOffset(
|
||||
pe.cli.metadata.rva,
|
||||
pe.cli.metadata.size,
|
||||
"cli.metadata",
|
||||
);
|
||||
if (pe.cli.metadata.size > limits.maxMetadataBytes)
|
||||
throw managedFailure(
|
||||
"limit-exceeded",
|
||||
"metadata.root",
|
||||
`CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
|
||||
rootOffset,
|
||||
);
|
||||
const layout = readManagedMetadataLayout(
|
||||
bytes,
|
||||
rootOffset,
|
||||
pe.cli.metadata.size,
|
||||
limits.maxTableRows,
|
||||
);
|
||||
const inventory = readManagedMetadataInventory(
|
||||
bytes,
|
||||
layout,
|
||||
{
|
||||
referenceOffset: 0,
|
||||
referenceLimit: 1,
|
||||
resourceOffset: 0,
|
||||
resourceLimit: 1,
|
||||
attributeOffset: 0,
|
||||
attributeLimit: 1,
|
||||
maxHeapItemBytes: limits.maxHeapItemBytes,
|
||||
},
|
||||
null,
|
||||
);
|
||||
issues.push(...inventory.issues);
|
||||
const moduleRefs = parseModuleRefs(bytes, layout, limits.maxHeapItemBytes);
|
||||
const fields = parseFields(bytes, layout, limits.maxHeapItemBytes);
|
||||
const methods = parseMethods(bytes, layout, limits.maxHeapItemBytes);
|
||||
const members = new Map([...fields, ...methods]);
|
||||
const imports = parseImplMaps(
|
||||
bytes,
|
||||
layout,
|
||||
limits.maxHeapItemBytes,
|
||||
moduleRefs,
|
||||
members,
|
||||
);
|
||||
const implementations = nativeImplementations(
|
||||
methods.values(),
|
||||
new Set(
|
||||
imports
|
||||
.map(({ member_token: token }) => token)
|
||||
.filter((token): token is string => token !== null),
|
||||
),
|
||||
);
|
||||
const native = cliNative(pe);
|
||||
return managedNativeBoundaryInspectionSchema.parse({
|
||||
schema_version: 1,
|
||||
artifact: {
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
byte_length: bytes.length,
|
||||
format: "pe",
|
||||
},
|
||||
module: inventory.module,
|
||||
metadata: {
|
||||
status: issues.length === 0 ? "complete" : "partial",
|
||||
version: layout.version,
|
||||
table_row_counts: managedTableRowCounts(layout),
|
||||
},
|
||||
identity_scope: {
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target.sha256,
|
||||
requires_mvid: inventory.module?.mvid ?? null,
|
||||
},
|
||||
cli_native: native,
|
||||
module_refs: page(
|
||||
moduleRefs,
|
||||
limits.moduleRefOffset,
|
||||
limits.moduleRefLimit,
|
||||
),
|
||||
pinvoke_imports: page(imports, limits.importOffset, limits.importLimit),
|
||||
native_implementations: page(
|
||||
implementations,
|
||||
limits.implementationOffset,
|
||||
limits.implementationLimit,
|
||||
),
|
||||
summary: {
|
||||
module_ref_count: moduleRefs.length,
|
||||
pinvoke_import_count: imports.length,
|
||||
native_implementation_count: implementations.length,
|
||||
ready_to_run: native.ready_to_run_signature,
|
||||
mixed_mode_or_native_header:
|
||||
native.managed_native_header_rva !== 0 ||
|
||||
native.managed_native_header_size !== 0 ||
|
||||
native.native_entry_point,
|
||||
},
|
||||
coverage: {
|
||||
state: issues.length === 0 ? "complete" : "partial",
|
||||
issues,
|
||||
},
|
||||
limitations: [
|
||||
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
|
||||
"Managed metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
|
||||
"ReadyToRun, NativeAOT, C++/CLI, and IL2CPP native semantics require separately selected native-provider evidence; this tool does not translate managed tokens into native addresses.",
|
||||
],
|
||||
});
|
||||
pe = readManagedPeLayout(bytes);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ManagedReaderFailure)
|
||||
return unavailable(target, bytes, limits, cause.issue);
|
||||
throw cause;
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
return emptyInspection(target, bytes, "malformed", [cause.issue]);
|
||||
}
|
||||
if (pe.cli === null)
|
||||
return emptyInspection(
|
||||
target,
|
||||
bytes,
|
||||
pe.cliDirectoryPresent ? "malformed" : "not-managed",
|
||||
pe.cliIssue === null ? [] : [pe.cliIssue],
|
||||
);
|
||||
if (pe.cli.metadata.size > limits.maxMetadataBytes)
|
||||
return emptyInspection(target, bytes, "malformed", [
|
||||
metadataLimitIssue(pe.cli, limits.maxMetadataBytes),
|
||||
]);
|
||||
let layout: ManagedMetadataLayout;
|
||||
let inventory: Inventory;
|
||||
try {
|
||||
({ layout, inventory } = readBoundaryInventory(bytes, pe, limits));
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof ManagedReaderFailure)) throw cause;
|
||||
return emptyInspection(target, bytes, "malformed", [cause.issue]);
|
||||
}
|
||||
const moduleRefs = parseModuleRefs(bytes, layout, limits.maxHeapItemBytes);
|
||||
const members = new Map([
|
||||
...parseFields(bytes, layout, limits.maxHeapItemBytes),
|
||||
...parseMethods(bytes, layout, limits.maxHeapItemBytes),
|
||||
]);
|
||||
const imports = parseImplMaps({
|
||||
bytes,
|
||||
layout,
|
||||
maxBytes: limits.maxHeapItemBytes,
|
||||
modules: moduleRefs,
|
||||
members,
|
||||
});
|
||||
const pinvokeTokens = new Set(
|
||||
imports
|
||||
.map(({ member_token }) => member_token)
|
||||
.filter((token): token is string => token !== null),
|
||||
);
|
||||
const implementations = nativeImplementations(
|
||||
members.values(),
|
||||
pinvokeTokens,
|
||||
);
|
||||
return buildNativeBoundaryInspection({
|
||||
target,
|
||||
bytes,
|
||||
limits,
|
||||
pe,
|
||||
layout,
|
||||
inventory,
|
||||
moduleRefs,
|
||||
imports,
|
||||
implementations,
|
||||
native: cliNative(pe),
|
||||
issues: [],
|
||||
});
|
||||
};
|
||||
|
||||
+104
-45
@@ -43,6 +43,29 @@ export interface ManagedPeLayout {
|
||||
rvaToOffset(rva: number, size: number, scope: string): number;
|
||||
}
|
||||
|
||||
interface RvaMapping {
|
||||
readonly bytes: Buffer;
|
||||
readonly sections: readonly PeSection[];
|
||||
readonly rva: number;
|
||||
readonly size: number;
|
||||
readonly scope: string;
|
||||
}
|
||||
|
||||
interface CoffHeader {
|
||||
readonly coff: number;
|
||||
readonly machine: number;
|
||||
readonly sectionCount: number;
|
||||
readonly optionalSize: number;
|
||||
readonly characteristics: number;
|
||||
}
|
||||
|
||||
interface OptionalHeader {
|
||||
readonly optionalOffset: number;
|
||||
readonly optionalHeader: "pe32" | "pe32-plus";
|
||||
readonly directoryCountOffset: number;
|
||||
readonly directoryOffset: number;
|
||||
}
|
||||
|
||||
const requireRange = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
@@ -95,13 +118,8 @@ const architectureFor = (machine: number): ManagedPeLayout["architecture"] => {
|
||||
}
|
||||
};
|
||||
|
||||
const mapRva = (
|
||||
bytes: Buffer,
|
||||
sections: readonly PeSection[],
|
||||
rva: number,
|
||||
size: number,
|
||||
scope: string,
|
||||
): number => {
|
||||
const mapRva = (mapping: RvaMapping): number => {
|
||||
const { bytes, sections, rva, size, scope } = mapping;
|
||||
if (rva === 0 && size === 0) return 0;
|
||||
for (const section of sections) {
|
||||
const mappedSize = Math.max(section.virtualSize, section.rawSize);
|
||||
@@ -144,7 +162,13 @@ const readCliHeader = (
|
||||
"cli.header",
|
||||
"CLI header is shorter than the required 72 bytes",
|
||||
);
|
||||
const offset = mapRva(bytes, sections, directory.rva, 72, "cli.header");
|
||||
const offset = mapRva({
|
||||
bytes,
|
||||
sections,
|
||||
rva: directory.rva,
|
||||
size: 72,
|
||||
scope: "cli.header",
|
||||
});
|
||||
const headerSize = bytes.readUInt32LE(offset);
|
||||
if (headerSize < 72 || headerSize > directory.size)
|
||||
throw managedFailure(
|
||||
@@ -165,13 +189,13 @@ const readCliHeader = (
|
||||
const managedNativeHeader = readDirectory(bytes, offset + 64);
|
||||
let readyToRunSignature = false;
|
||||
if (managedNativeHeader.rva !== 0 && managedNativeHeader.size >= 4) {
|
||||
const nativeOffset = mapRva(
|
||||
const nativeOffset = mapRva({
|
||||
bytes,
|
||||
sections,
|
||||
managedNativeHeader.rva,
|
||||
4,
|
||||
"cli.managed-native-header",
|
||||
);
|
||||
rva: managedNativeHeader.rva,
|
||||
size: 4,
|
||||
scope: "cli.managed-native-header",
|
||||
});
|
||||
readyToRunSignature = bytes.readUInt32LE(nativeOffset) === 0x0052_5452;
|
||||
}
|
||||
return {
|
||||
@@ -189,16 +213,7 @@ const readCliHeader = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Parse the file-backed PE and CLI layout without loading target code. */
|
||||
export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
requireRange(bytes, 0, 64, "pe.dos-header");
|
||||
if (bytes[0] !== 0x4d || bytes[1] !== 0x5a)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
"pe.dos-header",
|
||||
"Artifact does not start with an MZ header",
|
||||
0,
|
||||
);
|
||||
const readCoffHeader = (bytes: Buffer): CoffHeader => {
|
||||
const peOffset = bytes.readUInt32LE(0x3c);
|
||||
requireRange(bytes, peOffset, 24, "pe.coff-header");
|
||||
if (bytes.readUInt32LE(peOffset) !== 0x0000_4550)
|
||||
@@ -220,9 +235,13 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
);
|
||||
const optionalSize = bytes.readUInt16LE(coff + 16);
|
||||
const characteristics = bytes.readUInt16LE(coff + 18);
|
||||
const optionalOffset = coff + 20;
|
||||
requireRange(bytes, optionalOffset, optionalSize, "pe.optional-header");
|
||||
if (optionalSize < 96)
|
||||
return { coff, machine, sectionCount, optionalSize, characteristics };
|
||||
};
|
||||
|
||||
const readOptionalHeader = (bytes: Buffer, coff: CoffHeader): OptionalHeader => {
|
||||
const optionalOffset = coff.coff + 20;
|
||||
requireRange(bytes, optionalOffset, coff.optionalSize, "pe.optional-header");
|
||||
if (coff.optionalSize < 96)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
"pe.optional-header",
|
||||
@@ -239,16 +258,29 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
"PE optional header magic is unsupported",
|
||||
optionalOffset,
|
||||
);
|
||||
const directoryCountOffset =
|
||||
optionalOffset + (optionalHeader === "pe32" ? 92 : 108);
|
||||
const directoryOffset =
|
||||
optionalOffset + (optionalHeader === "pe32" ? 96 : 112);
|
||||
requireRange(bytes, directoryCountOffset, 4, "pe.data-directories");
|
||||
const directoryCount = bytes.readUInt32LE(directoryCountOffset);
|
||||
const sectionOffset = add(optionalOffset, optionalSize, "pe.sections");
|
||||
requireRange(bytes, sectionOffset, sectionCount * 40, "pe.sections");
|
||||
return {
|
||||
optionalOffset,
|
||||
optionalHeader,
|
||||
directoryCountOffset:
|
||||
optionalOffset + (optionalHeader === "pe32" ? 92 : 108),
|
||||
directoryOffset: optionalOffset + (optionalHeader === "pe32" ? 96 : 112),
|
||||
};
|
||||
};
|
||||
|
||||
const readSections = (
|
||||
bytes: Buffer,
|
||||
coff: CoffHeader,
|
||||
optional: OptionalHeader,
|
||||
): readonly PeSection[] => {
|
||||
const sectionOffset = add(optional.optionalOffset, coff.optionalSize, "pe.sections");
|
||||
requireRange(
|
||||
bytes,
|
||||
sectionOffset,
|
||||
coff.sectionCount * 40,
|
||||
"pe.sections",
|
||||
);
|
||||
const sections: PeSection[] = [];
|
||||
for (let index = 0; index < sectionCount; index += 1) {
|
||||
for (let index = 0; index < coff.sectionCount; index += 1) {
|
||||
const offset = sectionOffset + index * 40;
|
||||
const section = {
|
||||
virtualSize: bytes.readUInt32LE(offset + 8),
|
||||
@@ -265,11 +297,38 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
);
|
||||
sections.push(section);
|
||||
}
|
||||
const cliDirectory =
|
||||
return sections;
|
||||
};
|
||||
|
||||
const readCliDirectory = (
|
||||
bytes: Buffer,
|
||||
coff: CoffHeader,
|
||||
optional: OptionalHeader,
|
||||
): PeDataDirectory => {
|
||||
requireRange(bytes, optional.directoryCountOffset, 4, "pe.data-directories");
|
||||
const directoryCount = bytes.readUInt32LE(optional.directoryCountOffset);
|
||||
if (
|
||||
directoryCount <= 14 ||
|
||||
optionalSize < directoryOffset - optionalOffset + 120
|
||||
? { rva: 0, size: 0 }
|
||||
: readDirectory(bytes, directoryOffset + 14 * 8);
|
||||
coff.optionalSize < optional.directoryOffset - optional.optionalOffset + 120
|
||||
)
|
||||
return { rva: 0, size: 0 };
|
||||
return readDirectory(bytes, optional.directoryOffset + 14 * 8);
|
||||
};
|
||||
|
||||
/** Parse the file-backed PE and CLI layout without loading target code. */
|
||||
export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
requireRange(bytes, 0, 64, "pe.dos-header");
|
||||
if (bytes[0] !== 0x4d || bytes[1] !== 0x5a)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
"pe.dos-header",
|
||||
"Artifact does not start with an MZ header",
|
||||
0,
|
||||
);
|
||||
const coff = readCoffHeader(bytes);
|
||||
const optional = readOptionalHeader(bytes, coff);
|
||||
const sections = readSections(bytes, coff, optional);
|
||||
const cliDirectory = readCliDirectory(bytes, coff, optional);
|
||||
const cliDirectoryPresent = cliDirectory.rva !== 0 || cliDirectory.size !== 0;
|
||||
let cli: ManagedCliHeader | null = null;
|
||||
let cliIssue: ManagedParseIssue | null = null;
|
||||
@@ -281,16 +340,16 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
|
||||
cliIssue = cause.issue;
|
||||
}
|
||||
return {
|
||||
machine,
|
||||
architecture: architectureFor(machine),
|
||||
optionalHeader,
|
||||
sectionCount,
|
||||
characteristics,
|
||||
machine: coff.machine,
|
||||
architecture: architectureFor(coff.machine),
|
||||
optionalHeader: optional.optionalHeader,
|
||||
sectionCount: coff.sectionCount,
|
||||
characteristics: coff.characteristics,
|
||||
sections,
|
||||
cli,
|
||||
cliDirectoryPresent,
|
||||
cliIssue,
|
||||
rvaToOffset: (rva, size, scope) =>
|
||||
mapRva(bytes, sections, rva, size, scope),
|
||||
mapRva({ bytes, sections, rva, size, scope }),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user