Files
MercuryToolbox/crates/managed/src/lib.rs
T

2501 lines
79 KiB
Rust

//! Shared managed assembly metadata helpers.
use std::collections::{BTreeMap, HashMap};
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use clrmeta::reader::Reader;
use clrmeta::{
AssemblyInfo, AssemblyRefInfo, CodedIndex, CodedIndexKind, FieldSig, Metadata, MethodSig,
PropertySig, ResolvedType, TableId, TypeSig,
};
use goblin::pe::PE;
use goblin::pe::section_table::SectionTable;
use regex_lite::Regex;
use serde::Serialize;
use thiserror::Error;
mod flow;
mod reference_diagnose;
pub use flow::*;
pub use reference_diagnose::*;
/// Errors produced while inspecting managed assemblies.
#[derive(Debug, Error)]
pub enum ManagedError {
/// The assembly path could not be read.
#[error("failed to read {path}: {message}")]
Read {
/// Path that failed.
path: PathBuf,
/// Error detail.
message: String,
},
/// The file is not a PE image.
#[error("{path} is not a PE image")]
NotPe {
/// Path that failed.
path: PathBuf,
},
/// The PE image does not contain CLR metadata.
#[error("{path} is not a managed .NET assembly")]
NotManaged {
/// Path that failed.
path: PathBuf,
},
/// CLR metadata could not be parsed.
#[error("failed to parse CLR metadata from {path}: {message}")]
Metadata {
/// Path that failed.
path: PathBuf,
/// Error detail.
message: String,
},
/// The requested managed query could not be resolved uniquely.
#[error("{message}")]
Query {
/// Human-readable query failure detail.
message: String,
},
}
/// Filters for type queries.
#[derive(Debug, Clone, Default)]
pub struct TypeQuery {
/// Optional regex applied to the full type name.
pub match_pattern: Option<Regex>,
/// Optional regex applied to the namespace.
pub namespace_pattern: Option<Regex>,
/// Optional type-kind filter.
pub kind: Option<String>,
/// Restrict results to public or nested-public types.
pub public_only: bool,
/// Optional regex applied to the resolved base type full name.
pub base_pattern: Option<Regex>,
/// Optional regex applied to any resolved interface full name.
pub interface_pattern: Option<Regex>,
/// Maximum number of rows to return.
pub limit: Option<usize>,
}
/// Filters for member queries.
#[derive(Debug, Clone, Default)]
pub struct MemberQuery {
/// Type full names to inspect.
pub type_names: Vec<String>,
/// Optional member-kind filter.
pub kind: Option<String>,
/// Optional regex applied to member names.
pub match_pattern: Option<Regex>,
/// Binding-style visibility filter.
pub binding: BindingFilter,
/// Whether to include compiler special-name methods in method output.
pub include_special: bool,
/// Whether to hide compiler-generated backing fields and closure artifacts.
pub user_code_only: bool,
/// Maximum number of rows to return.
pub limit: Option<usize>,
}
/// Binding-style visibility and scope filters.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct BindingFilter {
/// Include public members.
pub include_public: bool,
/// Include non-public members.
pub include_non_public: bool,
/// Include instance members.
pub include_instance: bool,
/// Include static members.
pub include_static: bool,
}
impl Default for BindingFilter {
fn default() -> Self {
Self {
include_public: true,
include_non_public: false,
include_instance: true,
include_static: true,
}
}
}
/// Query settings for reference inspection.
#[derive(Debug, Clone, Default)]
pub struct ReferenceQuery {
/// Additional directories to search while resolving references.
pub resolve_dirs: Vec<PathBuf>,
}
/// Summary of one managed assembly.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssemblyDescriptor {
/// Assembly file path.
pub path: PathBuf,
/// Simple assembly name.
pub assembly_name: String,
/// Assembly version in dotted form.
pub assembly_version: Option<String>,
/// CLR metadata runtime version string.
pub runtime_version: String,
/// Whether the assembly contains IL-only code.
pub is_il_only: bool,
/// Whether the assembly is marked as a library.
pub is_library: bool,
/// Whether the assembly is strong-name signed.
pub is_strong_name_signed: bool,
/// Assembly public key token, when available.
pub public_key_token: Option<String>,
}
/// Type inspection output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TypeDescriptor {
/// Assembly file path.
pub assembly_path: PathBuf,
/// Simple assembly name.
pub assembly_name: String,
/// Full type name.
pub full_name: String,
/// Namespace, if any.
pub namespace: Option<String>,
/// Simple type name.
pub name: String,
/// Type kind label.
pub kind: String,
/// Visibility label.
pub visibility: String,
/// Whether the type is public or nested public.
pub is_public: bool,
/// Whether the type is abstract.
pub is_abstract: bool,
/// Whether the type is sealed.
pub is_sealed: bool,
/// Resolved base type full name, if any.
pub base_type: Option<String>,
/// Resolved interface full names.
pub interfaces: Vec<String>,
}
/// Parameter metadata for method or property signatures.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ParameterDescriptor {
/// Parameter name, if present in metadata.
pub name: Option<String>,
/// Rendered parameter type.
pub parameter_type: String,
}
/// Member inspection output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MemberDescriptor {
/// Method output.
Method {
/// Assembly file path.
assembly_path: PathBuf,
/// Simple assembly name.
assembly_name: String,
/// Declaring type full name.
type_name: String,
/// Method name.
name: String,
/// Visibility label.
visibility: String,
/// Whether the method is static.
is_static: bool,
/// Whether the method is virtual.
is_virtual: bool,
/// Whether the method is abstract.
is_abstract: bool,
/// Return type.
return_type: String,
/// Parameter list.
parameters: Vec<ParameterDescriptor>,
/// Human-readable signature.
signature: String,
},
/// Field output.
Field {
/// Assembly file path.
assembly_path: PathBuf,
/// Simple assembly name.
assembly_name: String,
/// Declaring type full name.
type_name: String,
/// Field name.
name: String,
/// Visibility label.
visibility: String,
/// Whether the field is static.
is_static: bool,
/// Whether the field is a literal constant.
is_literal: bool,
/// Whether the field is init-only.
is_init_only: bool,
/// Field type.
field_type: String,
/// Human-readable signature.
signature: String,
},
/// Property output.
Property {
/// Assembly file path.
assembly_path: PathBuf,
/// Simple assembly name.
assembly_name: String,
/// Declaring type full name.
type_name: String,
/// Property name.
name: String,
/// Effective visibility label.
visibility: String,
/// Whether the property is static.
is_static: bool,
/// Property type.
property_type: String,
/// Indexer parameters, if any.
parameters: Vec<ParameterDescriptor>,
/// Getter visibility, if present.
getter_visibility: Option<String>,
/// Setter visibility, if present.
setter_visibility: Option<String>,
/// Human-readable signature.
signature: String,
},
}
/// Visibility scope used by managed API diffing.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ApiVisibilityScope {
/// Public and nested-public types plus public methods.
#[default]
Public,
/// Public and assembly/protected API, excluding private members.
Internal,
/// Every metadata-visible type and method.
All,
}
impl ApiVisibilityScope {
/// Returns true when the type visibility is included in this scope.
#[must_use]
pub fn includes_type(self, visibility: &str) -> bool {
match self {
Self::Public => matches!(visibility, "public" | "nested_public"),
Self::Internal => !matches!(visibility, "nested_private"),
Self::All => true,
}
}
/// Returns true when the member visibility is included in this scope.
#[must_use]
pub fn includes_member(self, visibility: &str) -> bool {
match self {
Self::Public => visibility == "public",
Self::Internal => visibility != "private",
Self::All => true,
}
}
}
/// Query settings for managed API diffing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ApiDiffQuery {
/// Visibility scope to compare.
pub visibility: ApiVisibilityScope,
/// Include special-name methods such as property accessors.
pub include_special: bool,
/// Include `MissingMethodException` risk rows in the report.
pub include_missing_method_risks: bool,
}
impl Default for ApiDiffQuery {
fn default() -> Self {
Self {
visibility: ApiVisibilityScope::Public,
include_special: false,
include_missing_method_risks: true,
}
}
}
/// Normalized managed type row used by API diffing.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApiTypeRow {
/// Full type name.
pub full_name: String,
/// Namespace, if any.
pub namespace: Option<String>,
/// Simple type name.
pub name: String,
/// Type kind label.
pub kind: String,
/// Visibility label.
pub visibility: String,
/// Whether the type is public or nested public.
pub is_public: bool,
}
/// Normalized managed method row used by API diffing.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApiMethodRow {
/// Declaring type full name.
pub type_name: String,
/// Method name.
pub name: String,
/// Visibility label.
pub visibility: String,
/// Whether the method is static.
pub is_static: bool,
/// Whether the method is virtual.
pub is_virtual: bool,
/// Whether the method is abstract.
pub is_abstract: bool,
/// Return type.
pub return_type: String,
/// Parameter list.
pub parameters: Vec<ParameterDescriptor>,
/// Human-readable method signature.
pub signature: String,
}
/// Normalized API surface for one managed assembly.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssemblyApiSnapshot {
/// Assembly summary.
pub assembly: AssemblyDescriptor,
/// Normalized type rows.
pub types: Vec<ApiTypeRow>,
/// Normalized method rows.
pub methods: Vec<ApiMethodRow>,
}
/// Summary counts for a managed API diff report.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApiDiffSummary {
/// Removed type count.
pub removed_types: usize,
/// Added type count.
pub added_types: usize,
/// Removed method count.
pub removed_methods: usize,
/// Added method count.
pub added_methods: usize,
/// Changed method-name group count.
pub signature_changed_methods: usize,
/// `MissingMethodException` risk row count.
pub missing_method_risks: usize,
}
/// Added or removed type.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApiTypeChange {
/// Full type name.
pub type_name: String,
/// Type kind label.
pub kind: String,
/// Visibility label.
pub visibility: String,
}
/// Added or removed method, or a changed method-name group.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApiMethodChange {
/// Declaring type full name.
pub type_name: String,
/// Method name.
pub method_name: String,
/// Old signatures in this change.
pub old_signatures: Vec<String>,
/// New signatures in this change.
pub new_signatures: Vec<String>,
}
/// Likely `MissingMethodException` compatibility risk.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MissingMethodRisk {
/// Assembly that previously exposed the method.
pub old_assembly: String,
/// Declaring type full name.
pub type_name: String,
/// Method name.
pub method_name: String,
/// Old rendered method signature.
pub old_signature: String,
/// Risk reason.
pub reason: String,
}
/// Managed API diff report.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ApiDiffReport {
/// Old assembly summary.
pub old_assembly: AssemblyDescriptor,
/// New assembly summary.
pub new_assembly: AssemblyDescriptor,
/// Compared visibility scope.
pub visibility: ApiVisibilityScope,
/// Summary counts.
pub summary: ApiDiffSummary,
/// Removed types.
pub removed_types: Vec<ApiTypeChange>,
/// Added types.
pub added_types: Vec<ApiTypeChange>,
/// Removed methods.
pub removed_methods: Vec<ApiMethodChange>,
/// Added methods.
pub added_methods: Vec<ApiMethodChange>,
/// Changed method-name groups.
pub signature_changed_methods: Vec<ApiMethodChange>,
/// Likely `MissingMethodException` risks.
pub missing_method_risks: Vec<MissingMethodRisk>,
}
/// Resolved assembly reference output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssemblyReferenceDescriptor {
/// Referenced assembly name.
pub name: String,
/// Referenced version string.
pub version: String,
/// Referenced culture string, if present.
pub culture: Option<String>,
/// Public key token hex string, if present.
pub public_key_token: Option<String>,
/// Whether the reference resolved to a file path.
pub resolved: bool,
/// Resolved file path, if found.
pub resolved_path: Option<PathBuf>,
/// Resolution source label.
pub resolution_source: Option<String>,
/// Whether the reference looks like a framework assembly.
pub is_framework_reference: bool,
}
/// Report for one assembly and its references.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssemblyReferenceReport {
/// Assembly summary.
pub assembly: AssemblyDescriptor,
/// Reference rows.
pub references: Vec<AssemblyReferenceDescriptor>,
}
#[derive(Debug)]
struct ManagedAssembly {
path: PathBuf,
bytes: Vec<u8>,
sections: Vec<PeSectionSpan>,
metadata: Metadata,
descriptor: AssemblyDescriptor,
nested_parent_by_child: HashMap<u32, u32>,
}
#[derive(Debug, Clone, Copy)]
struct PeSectionSpan {
virtual_address: usize,
read_size: usize,
raw_offset: usize,
}
/// Lists managed types for one or more assembly paths.
///
/// # Errors
///
/// Returns [`ManagedError`] when an assembly cannot be read or parsed.
pub fn list_types(
paths: &[PathBuf],
query: &TypeQuery,
) -> Result<Vec<TypeDescriptor>, ManagedError> {
let mut rows = Vec::new();
for path in paths {
let assembly = ManagedAssembly::load(path)?;
let Some(last_type_index) = u32::try_from(assembly.metadata.type_defs.len()).ok() else {
return Ok(rows);
};
for index in 1..=last_type_index {
let descriptor = assembly.type_descriptor(index);
if type_matches(&descriptor, query) {
rows.push(descriptor);
if query.limit.is_some_and(|limit| rows.len() >= limit) {
return Ok(rows);
}
}
}
}
Ok(rows)
}
/// Lists managed members for one or more assemblies and type names.
///
/// # Errors
///
/// Returns [`ManagedError`] when an assembly cannot be read or parsed.
///
/// # Panics
///
/// This function does not panic.
pub fn list_members(
assembly_paths: &[PathBuf],
query: &MemberQuery,
) -> Result<Vec<MemberDescriptor>, ManagedError> {
let mut rows = Vec::new();
for path in assembly_paths {
let assembly = ManagedAssembly::load(path)?;
for type_name in &query.type_names {
if let Some(index) = assembly.find_type_index(type_name) {
let mut members = assembly.member_descriptors(index, query);
rows.append(&mut members);
if query.limit.is_some_and(|limit| rows.len() >= limit) {
if let Some(limit) = query.limit {
rows.truncate(limit);
}
return Ok(rows);
}
}
}
}
Ok(rows)
}
/// Inspects assembly references for one or more assemblies.
///
/// # Errors
///
/// Returns [`ManagedError`] when an assembly cannot be read or parsed.
pub fn inspect_references(
assembly_paths: &[PathBuf],
query: &ReferenceQuery,
) -> Result<Vec<AssemblyReferenceReport>, ManagedError> {
let mut reports = Vec::new();
for path in assembly_paths {
let assembly = ManagedAssembly::load(path)?;
reports.push(assembly.reference_report(query));
}
Ok(reports)
}
/// Builds a normalized managed API snapshot for one assembly.
///
/// # Errors
///
/// Returns [`ManagedError`] when the assembly cannot be read or parsed.
pub fn snapshot_assembly_api(
path: &Path,
query: &ApiDiffQuery,
) -> Result<AssemblyApiSnapshot, ManagedError> {
let assembly = ManagedAssembly::load(path)?;
let mut types = Vec::new();
let mut methods = Vec::new();
let Some(last_type_index) = u32::try_from(assembly.metadata.type_defs.len()).ok() else {
return Ok(AssemblyApiSnapshot {
assembly: assembly.descriptor,
types,
methods,
});
};
for index in 1..=last_type_index {
let type_row = ApiTypeRow::from(assembly.type_descriptor(index));
if query.visibility.includes_type(&type_row.visibility) {
methods.extend(assembly.api_method_rows(index, *query));
types.push(type_row);
}
}
Ok(AssemblyApiSnapshot {
assembly: assembly.descriptor,
types,
methods,
})
}
/// Diffs two managed assemblies after building API snapshots.
///
/// # Errors
///
/// Returns [`ManagedError`] when either assembly cannot be read or parsed.
pub fn diff_assembly_api(
old_path: &Path,
new_path: &Path,
query: &ApiDiffQuery,
) -> Result<ApiDiffReport, ManagedError> {
let old = snapshot_assembly_api(old_path, query)?;
let new = snapshot_assembly_api(new_path, query)?;
Ok(diff_api_snapshots(old, new, query))
}
/// Diffs two normalized managed API snapshots.
#[must_use]
pub fn diff_api_snapshots(
old: AssemblyApiSnapshot,
new: AssemblyApiSnapshot,
query: &ApiDiffQuery,
) -> ApiDiffReport {
let old_types = filtered_type_map(&old, query.visibility);
let new_types = filtered_type_map(&new, query.visibility);
let old_methods = filtered_method_groups(&old, query.visibility, &old_types);
let new_methods = filtered_method_groups(&new, query.visibility, &new_types);
let mut removed_types = old_types
.iter()
.filter(|(key, _)| !new_types.contains_key(*key))
.map(|(_, row)| ApiTypeChange::from(row))
.collect::<Vec<_>>();
let mut added_types = new_types
.iter()
.filter(|(key, _)| !old_types.contains_key(*key))
.map(|(_, row)| ApiTypeChange::from(row))
.collect::<Vec<_>>();
let mut removed_methods = Vec::new();
let mut added_methods = Vec::new();
let mut signature_changed_methods = Vec::new();
for (group, rows) in &old_methods {
if !new_methods.contains_key(group) {
removed_methods.extend(rows.iter().map(method_change_removed));
}
}
for (group, rows) in &new_methods {
if !old_methods.contains_key(group) {
added_methods.extend(rows.iter().map(method_change_added));
}
}
for (group, old_rows) in old_methods
.iter()
.filter(|(group, _)| new_methods.contains_key(*group))
{
let new_rows = &new_methods[group];
let old_signatures = method_signature_map(old_rows);
let new_signatures = method_signature_map(new_rows);
if old_signatures.keys().eq(new_signatures.keys()) {
continue;
}
signature_changed_methods.push(ApiMethodChange {
type_name: group.0.clone(),
method_name: group.1.clone(),
old_signatures: old_signatures
.values()
.map(|row| row.signature.clone())
.collect(),
new_signatures: new_signatures
.values()
.map(|row| row.signature.clone())
.collect(),
});
for key in old_signatures
.keys()
.filter(|key| !new_signatures.contains_key(*key))
{
if let Some(row) = old_signatures.get(key) {
removed_methods.push(method_change_removed(row));
}
}
for key in new_signatures
.keys()
.filter(|key| !old_signatures.contains_key(*key))
{
if let Some(row) = new_signatures.get(key) {
added_methods.push(method_change_added(row));
}
}
}
sort_type_changes(&mut removed_types);
sort_type_changes(&mut added_types);
sort_method_changes(&mut removed_methods);
sort_method_changes(&mut added_methods);
sort_method_changes(&mut signature_changed_methods);
removed_methods.dedup();
added_methods.dedup();
let missing_method_risks = if query.include_missing_method_risks {
missing_method_risks(&old.assembly, &removed_methods)
} else {
Vec::new()
};
let summary = ApiDiffSummary {
removed_types: removed_types.len(),
added_types: added_types.len(),
removed_methods: removed_methods.len(),
added_methods: added_methods.len(),
signature_changed_methods: signature_changed_methods.len(),
missing_method_risks: missing_method_risks.len(),
};
ApiDiffReport {
old_assembly: old.assembly,
new_assembly: new.assembly,
visibility: query.visibility,
summary,
removed_types,
added_types,
removed_methods,
added_methods,
signature_changed_methods,
missing_method_risks,
}
}
impl ManagedAssembly {
fn load(path: &Path) -> Result<Self, ManagedError> {
let bytes = fs::read(path).map_err(|error| ManagedError::Read {
path: path.to_path_buf(),
message: error.to_string(),
})?;
let pe = PE::parse(&bytes).map_err(|_| ManagedError::NotPe {
path: path.to_path_buf(),
})?;
let clr_data = pe.clr_data.ok_or_else(|| ManagedError::NotManaged {
path: path.to_path_buf(),
})?;
let metadata_bytes = metadata_bytes(&bytes, &pe, clr_data.cor20_header.metadata)
.ok_or_else(|| ManagedError::Metadata {
path: path.to_path_buf(),
message: "failed to map CLR metadata RVA into file bytes".to_string(),
})?;
let metadata = Metadata::parse(metadata_bytes).map_err(|error| ManagedError::Metadata {
path: path.to_path_buf(),
message: error.to_string(),
})?;
let assembly_info = metadata.assembly();
let assembly_name = assembly_info
.as_ref()
.map_or_else(|| file_stem(path), |info| info.name.clone());
let assembly_version = assembly_info.as_ref().map(AssemblyInfo::version_string);
let public_key_token = assembly_info
.as_ref()
.and_then(AssemblyInfo::public_key_token_string);
let descriptor = AssemblyDescriptor {
path: path.to_path_buf(),
assembly_name,
assembly_version,
runtime_version: metadata.version().to_string(),
is_il_only: clr_data.cor20_header.is_il_only(),
is_library: clr_data.cor20_header.is_il_library(),
is_strong_name_signed: clr_data.cor20_header.is_strong_name_signed(),
public_key_token,
};
let file_alignment = pe
.header
.optional_header
.map(|header| header.windows_fields.file_alignment)
.ok_or_else(|| ManagedError::Metadata {
path: path.to_path_buf(),
message: "missing PE optional header".to_string(),
})?;
let sections = pe
.sections
.iter()
.map(|section| PeSectionSpan {
virtual_address: section.virtual_address as usize,
read_size: section_read_size(section, file_alignment),
raw_offset: section.pointer_to_raw_data as usize,
})
.collect::<Vec<_>>();
let nested_parent_by_child = metadata
.nested_classes
.iter()
.map(|row| (row.nested_class, row.enclosing_class))
.collect::<HashMap<_, _>>();
Ok(Self {
path: path.to_path_buf(),
bytes,
sections,
metadata,
descriptor,
nested_parent_by_child,
})
}
fn bytes_at_rva(&self, rva: u32, size: usize) -> Option<&[u8]> {
let offset = self.offset_for_rva(rva)?;
self.bytes.get(offset..offset.checked_add(size)?)
}
fn offset_for_rva(&self, rva: u32) -> Option<usize> {
let rva = rva as usize;
self.sections.iter().find_map(|section| {
let end = section.virtual_address.checked_add(section.read_size)?;
if rva < section.virtual_address || rva >= end {
return None;
}
section
.raw_offset
.checked_add(rva - section.virtual_address)
})
}
fn type_descriptor(&self, index: u32) -> TypeDescriptor {
let row = self
.metadata
.get_type_def(index)
.expect("type index should be in range");
let name = self.string_or_empty(row.type_name);
let namespace = self.string_option(row.type_namespace);
let full_name = self.full_type_name(index);
let base_type = self
.metadata
.get_base_type(index)
.map(|base| self.resolved_type_name(&base));
let interfaces = self
.metadata
.get_interfaces(index)
.into_iter()
.map(|item| self.resolved_type_name(&item))
.collect::<Vec<_>>();
let visibility = type_visibility_label(row.flags).to_string();
let kind = self.type_kind(index).to_string();
TypeDescriptor {
assembly_path: self.descriptor.path.clone(),
assembly_name: self.descriptor.assembly_name.clone(),
full_name,
namespace,
name,
kind,
visibility,
is_public: is_public_type(row.flags),
is_abstract: (row.flags & 0x0000_0080) != 0,
is_sealed: (row.flags & 0x0000_0100) != 0,
base_type,
interfaces,
}
}
fn find_type_index(&self, full_name: &str) -> Option<u32> {
let last_type_index = u32::try_from(self.metadata.type_defs.len()).ok()?;
(1..=last_type_index).find(|index| self.full_type_name(*index) == full_name)
}
fn member_descriptors(&self, type_index: u32, query: &MemberQuery) -> Vec<MemberDescriptor> {
let type_name = self.full_type_name(type_index);
let mut rows = Vec::new();
let include_methods = member_kind_enabled(query.kind.as_deref(), "method");
let include_fields = member_kind_enabled(query.kind.as_deref(), "field");
let include_properties = member_kind_enabled(query.kind.as_deref(), "property");
if include_methods {
for item in self.method_descriptors(type_index, &type_name, query) {
if member_matches(&item, query) {
rows.push(item);
if query.limit.is_some_and(|limit| rows.len() >= limit) {
return rows;
}
}
}
}
if include_fields {
for item in self.field_descriptors(type_index, &type_name, query) {
if member_matches(&item, query) {
rows.push(item);
if query.limit.is_some_and(|limit| rows.len() >= limit) {
return rows;
}
}
}
}
if include_properties {
for item in self.property_descriptors(type_index, &type_name, query) {
if member_matches(&item, query) {
rows.push(item);
if query.limit.is_some_and(|limit| rows.len() >= limit) {
return rows;
}
}
}
}
rows
}
fn api_method_rows(&self, type_index: u32, query: ApiDiffQuery) -> Vec<ApiMethodRow> {
let type_name = self.full_type_name(type_index);
self.method_descriptors(
type_index,
&type_name,
&MemberQuery {
kind: Some("method".to_string()),
binding: BindingFilter {
include_public: true,
include_non_public: true,
include_instance: true,
include_static: true,
},
include_special: query.include_special,
..MemberQuery::default()
},
)
.into_iter()
.filter_map(|member| match member {
MemberDescriptor::Method {
type_name,
name,
visibility,
is_static,
is_virtual,
is_abstract,
return_type,
parameters,
signature,
..
} if query.visibility.includes_member(&visibility) => Some(ApiMethodRow {
type_name,
name,
visibility,
is_static,
is_virtual,
is_abstract,
return_type,
parameters,
signature,
}),
_ => None,
})
.collect()
}
fn method_descriptors(
&self,
type_index: u32,
type_name: &str,
query: &MemberQuery,
) -> Vec<MemberDescriptor> {
self.metadata
.get_type_methods(type_index)
.into_iter()
.filter_map(|(method_index, row)| {
if !query.include_special && (row.flags & 0x0800) != 0 {
return None;
}
if !query.binding.matches_method(row.flags) {
return None;
}
let name = self.string_or_empty(row.name);
let signature_bytes = self.metadata.blobs.get(row.signature).ok()?;
let signature = MethodSig::parse_blob(signature_bytes).ok()?;
let return_type = self.format_type_sig(&signature.return_type);
let parameters = self.method_parameters(method_index, &signature);
let signature_text = format_method_signature(&name, &return_type, &parameters);
Some(MemberDescriptor::Method {
assembly_path: self.descriptor.path.clone(),
assembly_name: self.descriptor.assembly_name.clone(),
type_name: type_name.to_string(),
name,
visibility: member_visibility_label(row.flags).to_string(),
is_static: (row.flags & 0x0010) != 0,
is_virtual: (row.flags & 0x0040) != 0,
is_abstract: (row.flags & 0x0400) != 0,
return_type,
parameters,
signature: signature_text,
})
})
.collect()
}
fn field_descriptors(
&self,
type_index: u32,
type_name: &str,
query: &MemberQuery,
) -> Vec<MemberDescriptor> {
self.metadata
.get_type_fields(type_index)
.into_iter()
.filter_map(|(_, row)| {
if !query.binding.matches_field(row.flags) {
return None;
}
let name = self.string_or_empty(row.name);
let signature_bytes = self.metadata.blobs.get(row.signature).ok()?;
let signature = FieldSig::parse_blob(signature_bytes).ok()?;
let field_type = self.format_type_sig(&signature.field_type);
let visibility = member_visibility_label(row.flags).to_string();
let signature_text = format_field_signature(
&field_type,
&name,
&visibility,
(row.flags & 0x0010) != 0,
(row.flags & 0x0040) != 0,
(row.flags & 0x0020) != 0,
);
Some(MemberDescriptor::Field {
assembly_path: self.descriptor.path.clone(),
assembly_name: self.descriptor.assembly_name.clone(),
type_name: type_name.to_string(),
name,
visibility,
is_static: (row.flags & 0x0010) != 0,
is_literal: (row.flags & 0x0040) != 0,
is_init_only: (row.flags & 0x0020) != 0,
field_type,
signature: signature_text,
})
})
.collect()
}
fn property_descriptors(
&self,
type_index: u32,
type_name: &str,
query: &MemberQuery,
) -> Vec<MemberDescriptor> {
self.get_type_properties(type_index)
.into_iter()
.filter_map(|(property_index, row)| {
let accessor_rows = self.property_accessors(property_index);
if !query.binding.matches_property(&accessor_rows) {
return None;
}
let name = self.string_or_empty(row.name);
let signature_bytes = self.metadata.blobs.get(row.property_type).ok()?;
let signature = PropertySig::parse_blob(signature_bytes).ok()?;
let property_type = self.format_type_sig(&signature.property_type);
let parameters = signature
.params
.iter()
.map(|param| ParameterDescriptor {
name: None,
parameter_type: self.format_type_sig(param),
})
.collect::<Vec<_>>();
let getter_visibility = accessor_rows.iter().find_map(|item| {
item.semantics
.contains("getter")
.then(|| item.visibility.clone())
});
let setter_visibility = accessor_rows.iter().find_map(|item| {
item.semantics
.contains("setter")
.then(|| item.visibility.clone())
});
let is_static = accessor_rows.iter().any(|item| item.is_static);
let visibility = getter_visibility
.clone()
.or_else(|| setter_visibility.clone())
.unwrap_or_else(|| "private".to_string());
let signature_text = format_property_signature(
&property_type,
&name,
&parameters,
getter_visibility.as_deref(),
setter_visibility.as_deref(),
is_static,
);
Some(MemberDescriptor::Property {
assembly_path: self.descriptor.path.clone(),
assembly_name: self.descriptor.assembly_name.clone(),
type_name: type_name.to_string(),
name,
visibility,
is_static,
property_type,
parameters,
getter_visibility,
setter_visibility,
signature: signature_text,
})
})
.collect()
}
fn reference_report(&self, query: &ReferenceQuery) -> AssemblyReferenceReport {
let resolve_dirs = self
.path
.parent()
.map(Path::to_path_buf)
.into_iter()
.chain(query.resolve_dirs.iter().cloned())
.collect::<Vec<_>>();
let references = self
.metadata
.assembly_refs()
.into_iter()
.map(|item| resolve_reference(&item, &resolve_dirs))
.collect::<Vec<_>>();
AssemblyReferenceReport {
assembly: self.descriptor.clone(),
references,
}
}
fn full_type_name(&self, index: u32) -> String {
let row = self
.metadata
.get_type_def(index)
.expect("type index should be in range");
let name = self.string_or_empty(row.type_name);
if let Some(parent) = self.nested_parent_by_child.get(&index) {
return format!("{}+{}", self.full_type_name(*parent), name);
}
let namespace = self.string_option(row.type_namespace);
if let Some(namespace) = namespace
&& !namespace.is_empty()
{
return format!("{namespace}.{name}");
}
name
}
fn type_kind(&self, index: u32) -> &'static str {
let row = self
.metadata
.get_type_def(index)
.expect("type index should be in range");
if (row.flags & 0x20) != 0 {
return "interface";
}
match self
.metadata
.get_base_type(index)
.map(|item| self.resolved_type_name(&item))
{
Some(base) if base == "System.Enum" => "enum",
Some(base) if base == "System.MulticastDelegate" => "delegate",
Some(base) if base == "System.ValueType" => "struct",
_ => "class",
}
}
fn resolved_type_name(&self, resolved: &ResolvedType) -> String {
match resolved {
ResolvedType::TypeSpec { signature, .. } => {
let signature_bytes = self.metadata.blobs.get(*signature).ok();
signature_bytes
.and_then(parse_type_sig_blob)
.map_or_else(|| resolved.full_name(), |sig| self.format_type_sig(&sig))
}
_ => resolved.full_name(),
}
}
fn format_type_sig(&self, signature: &TypeSig) -> String {
match signature {
TypeSig::Primitive(element) => element.name().to_string(),
TypeSig::Class(token) | TypeSig::ValueType(token) => self
.metadata
.resolve_type(&CodedIndex::decode(CodedIndexKind::TypeDefOrRef, *token))
.map_or_else(
|| format!("<token:{token}>"),
|item| self.resolved_type_name(&item),
),
TypeSig::SzArray(inner) => format!("{}[]", self.format_type_sig(inner)),
TypeSig::Array {
element_type, rank, ..
} => {
format!(
"{}[{}]",
self.format_type_sig(element_type),
",".repeat(rank.saturating_sub(1) as usize)
)
}
TypeSig::Ptr(inner) => format!("{}*", self.format_type_sig(inner)),
TypeSig::ByRef(inner) => format!("{}&", self.format_type_sig(inner)),
TypeSig::GenericInst {
is_value_type: _,
type_ref,
type_args,
} => {
let base = self
.metadata
.resolve_type(&CodedIndex::decode(CodedIndexKind::TypeDefOrRef, *type_ref))
.map_or_else(
|| format!("<token:{type_ref}>"),
|item| self.resolved_type_name(&item),
);
let args = type_args
.iter()
.map(|item| self.format_type_sig(item))
.collect::<Vec<_>>()
.join(", ");
format!("{base}<{args}>")
}
TypeSig::Var(index) => format!("!{index}"),
TypeSig::MVar(index) => format!("!!{index}"),
TypeSig::FnPtr(method) => format!("fnptr {}", self.format_method_sig(method, "invoke")),
TypeSig::Modified {
required,
modifier,
inner,
} => {
let label = if *required { "modreq" } else { "modopt" };
format!(
"{label}(<token:{modifier}>) {}",
self.format_type_sig(inner)
)
}
TypeSig::Pinned(inner) => format!("pinned {}", self.format_type_sig(inner)),
_ => "<unsupported>".to_string(),
}
}
fn format_method_sig(&self, signature: &MethodSig, name: &str) -> String {
let return_type = self.format_type_sig(&signature.return_type);
let parameters = signature
.params
.iter()
.map(|item| ParameterDescriptor {
name: None,
parameter_type: self.format_type_sig(item),
})
.collect::<Vec<_>>();
format_method_signature(name, &return_type, &parameters)
}
fn method_parameters(
&self,
method_index: u32,
signature: &MethodSig,
) -> Vec<ParameterDescriptor> {
let param_rows = self.get_method_params(method_index);
signature
.params
.iter()
.enumerate()
.map(|(index, item)| {
let name = param_rows
.iter()
.find(|(_, row)| usize::from(row.sequence) == index + 1)
.map(|(_, row)| self.string_or_empty(row.name));
ParameterDescriptor {
name,
parameter_type: self.format_type_sig(item),
}
})
.collect()
}
fn get_method_params(&self, method_index: u32) -> Vec<(u32, &clrmeta::ParamRow)> {
let Some(row) = method_index
.checked_sub(1)
.and_then(|index| self.metadata.method_defs.get(index as usize))
else {
return Vec::new();
};
let start = row.param_list;
let end = method_index
.checked_add(1)
.and_then(|next_index| self.metadata.method_defs.get((next_index - 1) as usize))
.map_or_else(
|| length_index(&self.metadata.params),
|item| item.param_list,
);
((start as usize)..(end as usize))
.filter_map(|index| {
if index > 0 && index <= self.metadata.params.len() {
Some((u32::try_from(index).ok()?, &self.metadata.params[index - 1]))
} else {
None
}
})
.collect()
}
fn get_type_properties(&self, type_index: u32) -> Vec<(u32, &clrmeta::PropertyRow)> {
let Some(start) = self
.metadata
.property_maps
.iter()
.find(|row| row.parent == type_index)
.map(|row| row.property_list)
else {
return Vec::new();
};
let end = self
.metadata
.property_maps
.iter()
.filter(|row| row.parent > type_index)
.map(|row| row.property_list)
.min()
.unwrap_or_else(|| length_index(&self.metadata.properties));
((start as usize)..(end as usize))
.filter_map(|index| {
if index > 0 && index <= self.metadata.properties.len() {
Some((
u32::try_from(index).ok()?,
&self.metadata.properties[index - 1],
))
} else {
None
}
})
.collect()
}
fn property_accessors(&self, property_index: u32) -> Vec<PropertyAccessor> {
self.metadata
.method_semantics
.iter()
.filter(|row| {
row.association.table == Some(TableId::Property)
&& row.association.row == property_index
})
.filter_map(|row| {
let method = row
.method
.checked_sub(1)
.and_then(|index| self.metadata.method_defs.get(index as usize))?;
Some(PropertyAccessor {
visibility: member_visibility_label(method.flags).to_string(),
is_static: (method.flags & 0x0010) != 0,
semantics: method_semantics_label(row.semantics),
})
})
.collect()
}
fn string_or_empty(&self, index: u32) -> String {
self.metadata
.strings
.get(index)
.map_or_else(|_| String::new(), ToString::to_string)
}
fn string_option(&self, index: u32) -> Option<String> {
if index == 0 {
return None;
}
self.metadata
.strings
.get(index)
.ok()
.map(ToString::to_string)
}
}
#[derive(Debug, Clone)]
struct PropertyAccessor {
visibility: String,
is_static: bool,
semantics: String,
}
fn resolve_reference(
reference: &AssemblyRefInfo,
resolve_dirs: &[PathBuf],
) -> AssemblyReferenceDescriptor {
let resolved = resolve_dirs.iter().find_map(|directory| {
let dll_path = directory.join(format!("{}.dll", reference.name));
if dll_path.is_file() {
return Some((dll_path, "resolve_dir".to_string()));
}
let exe_path = directory.join(format!("{}.exe", reference.name));
exe_path
.is_file()
.then(|| (exe_path, "resolve_dir".to_string()))
});
let (resolved_path, resolution_source, resolved_flag) = match resolved {
Some((path, source)) => (Some(path), Some(source), true),
None => (None, None, false),
};
AssemblyReferenceDescriptor {
name: reference.name.clone(),
version: reference.version_string(),
culture: reference.culture.clone(),
public_key_token: reference
.public_key_token
.as_ref()
.map(|value| bytes_to_hex(value)),
resolved: resolved_flag,
resolved_path,
resolution_source,
is_framework_reference: is_framework_reference(&reference.name),
}
}
fn type_matches(descriptor: &TypeDescriptor, query: &TypeQuery) -> bool {
if query.public_only && !descriptor.is_public {
return false;
}
if let Some(kind) = &query.kind
&& descriptor.kind != *kind
{
return false;
}
if let Some(pattern) = &query.match_pattern
&& !pattern.is_match(&descriptor.full_name)
{
return false;
}
if let Some(pattern) = &query.namespace_pattern {
let namespace = descriptor.namespace.as_deref().unwrap_or_default();
if !pattern.is_match(namespace) {
return false;
}
}
if let Some(pattern) = &query.base_pattern {
let base = descriptor.base_type.as_deref().unwrap_or_default();
if !pattern.is_match(base) {
return false;
}
}
if let Some(pattern) = &query.interface_pattern
&& !descriptor
.interfaces
.iter()
.any(|item| pattern.is_match(item))
{
return false;
}
true
}
fn member_matches(descriptor: &MemberDescriptor, query: &MemberQuery) -> bool {
let name = match descriptor {
MemberDescriptor::Method { name, .. }
| MemberDescriptor::Field { name, .. }
| MemberDescriptor::Property { name, .. } => name,
};
if query.user_code_only && is_compiler_generated_member_name(name) {
return false;
}
if let Some(pattern) = &query.match_pattern {
if !pattern.is_match(name) {
return false;
}
}
true
}
impl From<TypeDescriptor> for ApiTypeRow {
fn from(value: TypeDescriptor) -> Self {
Self {
full_name: value.full_name,
namespace: value.namespace,
name: value.name,
kind: value.kind,
visibility: value.visibility,
is_public: value.is_public,
}
}
}
impl From<&ApiTypeRow> for ApiTypeChange {
fn from(value: &ApiTypeRow) -> Self {
Self {
type_name: value.full_name.clone(),
kind: value.kind.clone(),
visibility: value.visibility.clone(),
}
}
}
fn filtered_type_map(
snapshot: &AssemblyApiSnapshot,
visibility: ApiVisibilityScope,
) -> BTreeMap<String, ApiTypeRow> {
snapshot
.types
.iter()
.filter(|row| visibility.includes_type(&row.visibility))
.map(|row| (row.full_name.clone(), row.clone()))
.collect()
}
fn filtered_method_groups(
snapshot: &AssemblyApiSnapshot,
visibility: ApiVisibilityScope,
types: &BTreeMap<String, ApiTypeRow>,
) -> BTreeMap<(String, String), Vec<ApiMethodRow>> {
let mut groups: BTreeMap<(String, String), Vec<ApiMethodRow>> = BTreeMap::new();
for row in &snapshot.methods {
if types.contains_key(&row.type_name) && visibility.includes_member(&row.visibility) {
groups
.entry((row.type_name.clone(), row.name.clone()))
.or_default()
.push(row.clone());
}
}
for rows in groups.values_mut() {
rows.sort_by(|left, right| method_sort_key(left).cmp(&method_sort_key(right)));
}
groups
}
fn method_signature_map(rows: &[ApiMethodRow]) -> BTreeMap<String, ApiMethodRow> {
rows.iter()
.map(|row| (method_fingerprint(row), row.clone()))
.collect()
}
fn method_fingerprint(row: &ApiMethodRow) -> String {
let parameters = row
.parameters
.iter()
.map(|parameter| parameter.parameter_type.as_str())
.collect::<Vec<_>>()
.join("\u{1f}");
format!(
"static={} return={} params={}",
row.is_static, row.return_type, parameters
)
}
fn method_change_removed(row: &ApiMethodRow) -> ApiMethodChange {
ApiMethodChange {
type_name: row.type_name.clone(),
method_name: row.name.clone(),
old_signatures: vec![row.signature.clone()],
new_signatures: Vec::new(),
}
}
fn method_change_added(row: &ApiMethodRow) -> ApiMethodChange {
ApiMethodChange {
type_name: row.type_name.clone(),
method_name: row.name.clone(),
old_signatures: Vec::new(),
new_signatures: vec![row.signature.clone()],
}
}
fn missing_method_risks(
old_assembly: &AssemblyDescriptor,
removed_methods: &[ApiMethodChange],
) -> Vec<MissingMethodRisk> {
let mut rows = removed_methods
.iter()
.flat_map(|change| {
change
.old_signatures
.iter()
.map(|signature| MissingMethodRisk {
old_assembly: old_assembly.assembly_name.clone(),
type_name: change.type_name.clone(),
method_name: change.method_name.clone(),
old_signature: signature.clone(),
reason: "removed_or_changed_public_signature".to_string(),
})
})
.collect::<Vec<_>>();
rows.sort_by(|left, right| {
(
left.type_name.as_str(),
left.method_name.as_str(),
left.old_signature.as_str(),
)
.cmp(&(
right.type_name.as_str(),
right.method_name.as_str(),
right.old_signature.as_str(),
))
});
rows.dedup();
rows
}
fn sort_type_changes(rows: &mut [ApiTypeChange]) {
rows.sort_by(|left, right| left.type_name.cmp(&right.type_name));
}
fn sort_method_changes(rows: &mut [ApiMethodChange]) {
rows.sort_by(|left, right| {
(
left.type_name.as_str(),
left.method_name.as_str(),
left.old_signatures.first().map_or("", String::as_str),
left.new_signatures.first().map_or("", String::as_str),
)
.cmp(&(
right.type_name.as_str(),
right.method_name.as_str(),
right.old_signatures.first().map_or("", String::as_str),
right.new_signatures.first().map_or("", String::as_str),
))
});
}
fn method_sort_key(row: &ApiMethodRow) -> (&str, &str, bool, String) {
(
row.type_name.as_str(),
row.name.as_str(),
row.is_static,
row.parameters
.iter()
.map(|parameter| parameter.parameter_type.as_str())
.collect::<Vec<_>>()
.join("\u{1f}"),
)
}
fn is_compiler_generated_member_name(name: &str) -> bool {
name.starts_with('<')
|| name.starts_with("<>")
|| name.starts_with("CS$<")
|| name.contains(">k__BackingField")
}
fn member_kind_enabled(kind: Option<&str>, label: &str) -> bool {
match kind {
None | Some("all") => true,
Some(value) => value == label,
}
}
const fn is_public_type(flags: u32) -> bool {
matches!(flags & 0x0000_0007, 0x0000_0001 | 0x0000_0002)
}
const fn type_visibility_label(flags: u32) -> &'static str {
match flags & 0x0000_0007 {
0x0000_0001 => "public",
0x0000_0002 => "nested_public",
0x0000_0003 => "nested_private",
0x0000_0004 => "nested_family",
0x0000_0005 => "nested_assembly",
0x0000_0006 => "nested_fam_and_assem",
0x0000_0007 => "nested_fam_or_assem",
_ => "not_public",
}
}
const fn member_visibility_label(flags: u16) -> &'static str {
match flags & 0x0007 {
0x0001 => "private",
0x0002 => "fam_and_assem",
0x0003 => "assembly",
0x0004 => "family",
0x0005 => "fam_or_assem",
0x0006 => "public",
_ => "compiler_controlled",
}
}
fn method_semantics_label(flags: u16) -> String {
let mut parts = Vec::new();
if (flags & 0x0001) != 0 {
parts.push("setter");
}
if (flags & 0x0002) != 0 {
parts.push("getter");
}
if (flags & 0x0004) != 0 {
parts.push("other");
}
if (flags & 0x0008) != 0 {
parts.push("add_on");
}
if (flags & 0x0010) != 0 {
parts.push("remove_on");
}
if (flags & 0x0020) != 0 {
parts.push("fire");
}
if parts.is_empty() {
"other".to_string()
} else {
parts.join("|")
}
}
fn format_method_signature(
name: &str,
return_type: &str,
parameters: &[ParameterDescriptor],
) -> String {
let args = parameters
.iter()
.map(|item| {
item.name.as_ref().map_or_else(
|| item.parameter_type.clone(),
|name| format!("{} {name}", item.parameter_type),
)
})
.collect::<Vec<_>>()
.join(", ");
format!("{return_type} {name}({args})")
}
fn format_field_signature(
field_type: &str,
name: &str,
visibility: &str,
is_static: bool,
is_literal: bool,
is_init_only: bool,
) -> String {
let mut prefixes = Vec::new();
prefixes.push(visibility.to_string());
if is_static {
prefixes.push("static".to_string());
}
if is_literal {
prefixes.push("literal".to_string());
}
if is_init_only {
prefixes.push("initonly".to_string());
}
format!("{} {field_type} {name}", prefixes.join(" "))
}
fn format_property_signature(
property_type: &str,
name: &str,
parameters: &[ParameterDescriptor],
getter_visibility: Option<&str>,
setter_visibility: Option<&str>,
is_static: bool,
) -> String {
let property_name = if parameters.is_empty() {
name.to_string()
} else {
let args = parameters
.iter()
.map(|item| item.parameter_type.clone())
.collect::<Vec<_>>()
.join(", ");
format!("{name}[{args}]")
};
let mut accessors = Vec::new();
if let Some(visibility) = getter_visibility {
accessors.push(accessor_signature("get", visibility));
}
if let Some(visibility) = setter_visibility {
accessors.push(accessor_signature("set", visibility));
}
let static_prefix = if is_static { "static " } else { "" };
format!(
"{static_prefix}{property_type} {property_name} {{ {} }}",
accessors.join(" ")
)
}
fn accessor_signature(name: &str, visibility: &str) -> String {
if visibility == "public" {
format!("{name};")
} else {
format!("{visibility} {name};")
}
}
fn file_stem(path: &Path) -> String {
path.file_stem()
.and_then(|value| value.to_str())
.map_or_else(|| path.display().to_string(), ToString::to_string)
}
fn is_framework_reference(name: &str) -> bool {
name == "mscorlib"
|| name == "netstandard"
|| name.starts_with("System")
|| name.starts_with("Microsoft.")
}
fn bytes_to_hex(bytes: &[u8]) -> String {
let mut text = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(text, "{byte:02x}");
}
text
}
fn parse_type_sig_blob(bytes: &[u8]) -> Option<TypeSig> {
let mut reader = Reader::new(bytes);
TypeSig::parse(&mut reader).ok()
}
fn metadata_bytes<'a>(
bytes: &'a [u8],
pe: &PE<'_>,
directory: goblin::pe::data_directories::DataDirectory,
) -> Option<&'a [u8]> {
let file_alignment = pe
.header
.optional_header
.map(|header| header.windows_fields.file_alignment)?;
let offset = find_pe_offset(
directory.virtual_address as usize,
&pe.sections,
file_alignment,
)?;
let size = directory.size as usize;
bytes.get(offset..offset.checked_add(size)?)
}
fn find_pe_offset(rva: usize, sections: &[SectionTable], file_alignment: u32) -> Option<usize> {
sections.iter().find_map(|section| {
let start = section.virtual_address as usize;
let read_size = section_read_size(section, file_alignment);
let end = start.checked_add(read_size)?;
if rva < start || rva >= end {
return None;
}
let raw = section.pointer_to_raw_data as usize;
raw.checked_add(rva - start)
})
}
fn section_read_size(section: &SectionTable, file_alignment: u32) -> usize {
let raw_size = section.size_of_raw_data as usize;
let virtual_size = section.virtual_size as usize;
if file_alignment < 0x200 {
return raw_size.max(virtual_size);
}
if virtual_size == 0 {
return raw_size;
}
raw_size.max(virtual_size)
}
fn length_index<T>(items: &[T]) -> u32 {
u32::try_from(items.len().saturating_add(1)).unwrap_or(u32::MAX)
}
impl BindingFilter {
/// Returns true when the method matches the filter.
#[must_use]
pub fn matches_method(self, flags: u16) -> bool {
self.matches_common(flags)
}
/// Returns true when the field matches the filter.
#[must_use]
pub fn matches_field(self, flags: u16) -> bool {
self.matches_common(flags)
}
/// Returns true when any accessor of a property matches the filter.
#[must_use]
fn matches_property(self, accessors: &[PropertyAccessor]) -> bool {
accessors.iter().any(|item| {
self.matches_visibility(&item.visibility) && self.matches_scope(item.is_static)
})
}
fn matches_common(self, flags: u16) -> bool {
self.matches_visibility(member_visibility_label(flags))
&& self.matches_scope((flags & 0x0010) != 0)
}
fn matches_visibility(self, visibility: &str) -> bool {
let is_public = visibility == "public";
(is_public && self.include_public) || (!is_public && self.include_non_public)
}
const fn matches_scope(self, is_static: bool) -> bool {
(is_static && self.include_static) || (!is_static && self.include_instance)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use regex_lite::Regex;
use super::*;
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("workspace root")
}
fn managed_fixture_paths() -> Vec<PathBuf> {
let root = workspace_root()
.join("fixtures")
.join("managed")
.join("bin");
vec![root.join("GameAssembly.dll")]
}
fn fixture_support_dir() -> PathBuf {
workspace_root()
.join("fixtures")
.join("managed")
.join("bin")
}
fn diagnose_fixture_root() -> PathBuf {
workspace_root()
.join("fixtures")
.join("managed")
.join("diagnose-bin")
.join("root")
.join("RootPlugin.dll")
}
fn diagnose_server_a_dir() -> PathBuf {
workspace_root()
.join("fixtures")
.join("managed")
.join("diagnose-bin")
.join("server-a")
}
fn diagnose_server_b_dir() -> PathBuf {
workspace_root()
.join("fixtures")
.join("managed")
.join("diagnose-bin")
.join("server-b")
}
#[test]
fn binding_filter_matches_expected_visibility_and_scope() {
let filter = BindingFilter {
include_public: true,
include_non_public: false,
include_instance: true,
include_static: false,
};
assert!(filter.matches_method(0x0006));
assert!(!filter.matches_method(0x0016));
assert!(!filter.matches_method(0x0001));
}
#[test]
fn formatting_helpers_render_compact_signatures() {
let parameters = vec![
ParameterDescriptor {
name: Some("count".to_string()),
parameter_type: "int".to_string(),
},
ParameterDescriptor {
name: Some("tag".to_string()),
parameter_type: "string".to_string(),
},
];
assert_eq!(
format_method_signature("StartProject", "void", &parameters),
"void StartProject(int count, string tag)"
);
assert_eq!(
format_field_signature("int", "_buildTicks", "private", false, false, true),
"private initonly int _buildTicks"
);
assert_eq!(
format_property_signature(
"string",
"ProjectName",
&[],
Some("public"),
Some("private"),
false
),
"string ProjectName { get; private set; }"
);
}
#[test]
fn type_and_member_label_helpers_are_stable() {
assert_eq!(type_visibility_label(0x0000_0001), "public");
assert_eq!(type_visibility_label(0x0000_0003), "nested_private");
assert_eq!(member_visibility_label(0x0006), "public");
assert_eq!(member_visibility_label(0x0001), "private");
assert_eq!(method_semantics_label(0x0003), "setter|getter");
assert!(is_framework_reference("System.Runtime"));
assert!(!is_framework_reference("FixtureSupport"));
}
#[test]
fn list_types_filters_by_name_kind_namespace_and_limit() {
let paths = managed_fixture_paths();
let broad_query = TypeQuery {
match_pattern: Some(Regex::new("(?i)spacecraft").expect("regex")),
namespace_pattern: None,
kind: None,
public_only: false,
base_pattern: None,
interface_pattern: None,
limit: None,
};
let broad_types = list_types(&paths, &broad_query).expect("managed types");
assert!(broad_types.iter().any(|item| {
item.full_name == "Game.UI.Windows.Windows.SpaceCraftConstructionWindow"
&& item.kind == "class"
}));
assert!(broad_types.iter().any(|item| {
item.full_name == "Data.SpacecraftConstructData" && item.kind == "struct"
}));
let filtered_query = TypeQuery {
match_pattern: Some(Regex::new("(?i)spacecraft").expect("regex")),
namespace_pattern: Some(Regex::new(r"(?i)^Game\.UI").expect("regex")),
kind: Some("class".to_string()),
public_only: true,
base_pattern: Some(
Regex::new("(?i)MercuryFixture.Support.LaunchVehicleBase").expect("regex"),
),
interface_pattern: Some(
Regex::new("(?i)MercuryFixture.Support.ILaunchable").expect("regex"),
),
limit: Some(1),
};
let filtered = list_types(&paths, &filtered_query).expect("filtered managed types");
assert_eq!(filtered.len(), 1);
assert_eq!(
filtered[0].full_name,
"Game.UI.Windows.Windows.SpaceCraftConstructionWindow"
);
}
#[test]
fn list_members_covers_kind_binding_and_special_name_filters() {
let paths = managed_fixture_paths();
let type_name = "Game.UI.Windows.Windows.SpaceCraftConstructionWindow".to_string();
let all_members = list_members(
&paths,
&MemberQuery {
type_names: vec![type_name.clone()],
kind: None,
match_pattern: Some(
Regex::new("(?i)build|project|launch|queue|complete|projectname")
.expect("regex"),
),
binding: BindingFilter {
include_public: true,
include_non_public: true,
include_instance: true,
include_static: true,
},
include_special: false,
user_code_only: false,
limit: None,
},
)
.expect("all members");
assert!(all_members.iter().any(|item| matches!(
item,
MemberDescriptor::Method { name, .. } if name == "StartProject"
)));
assert!(all_members.iter().any(|item| matches!(
item,
MemberDescriptor::Field { name, .. } if name == "_buildTicks"
)));
assert!(all_members.iter().any(|item| matches!(
item,
MemberDescriptor::Property { name, .. } if name == "ProjectName"
)));
assert!(!all_members.iter().any(|item| matches!(
item,
MemberDescriptor::Method { name, .. } if name == "get_ProjectName"
)));
let special_members = list_members(
&paths,
&MemberQuery {
type_names: vec![type_name],
kind: Some("method".to_string()),
match_pattern: Some(Regex::new("(?i)projectname").expect("regex")),
binding: BindingFilter {
include_public: true,
include_non_public: true,
include_instance: true,
include_static: false,
},
include_special: true,
user_code_only: false,
limit: None,
},
)
.expect("special members");
assert!(special_members.iter().any(|item| matches!(
item,
MemberDescriptor::Method { name, .. } if name == "get_ProjectName"
)));
let filtered_members = list_members(
&paths,
&MemberQuery {
type_names: vec![
"Game.UI.Windows.Windows.SpaceCraftConstructionWindow".to_string(),
],
kind: Some("field".to_string()),
match_pattern: Some(Regex::new("(?i)projectname").expect("regex")),
binding: BindingFilter {
include_public: true,
include_non_public: true,
include_instance: true,
include_static: true,
},
include_special: false,
user_code_only: true,
limit: None,
},
)
.expect("filtered members");
assert!(!filtered_members.iter().any(|item| matches!(
item,
MemberDescriptor::Field { name, .. } if name.contains("BackingField")
)));
}
#[test]
fn inspect_references_resolves_fixture_support_dependency() {
let paths = managed_fixture_paths();
let reports = inspect_references(
&paths,
&ReferenceQuery {
resolve_dirs: vec![fixture_support_dir()],
},
)
.expect("reference reports");
assert_eq!(reports.len(), 1);
let fixture_support = reports[0]
.references
.iter()
.find(|item| item.name == "FixtureSupport")
.expect("FixtureSupport reference");
assert!(fixture_support.resolved);
assert!(
fixture_support
.resolved_path
.as_ref()
.is_some_and(|path| path.ends_with("FixtureSupport.dll"))
);
}
fn api_type(full_name: &str, visibility: &str, is_public: bool) -> ApiTypeRow {
ApiTypeRow {
full_name: full_name.to_string(),
namespace: full_name
.rsplit_once('.')
.map(|(namespace, _)| namespace.to_string()),
name: full_name
.rsplit_once('.')
.map_or(full_name, |(_, name)| name)
.to_string(),
kind: "class".to_string(),
visibility: visibility.to_string(),
is_public,
}
}
fn api_method(
type_name: &str,
name: &str,
visibility: &str,
is_static: bool,
return_type: &str,
parameters: &[&str],
) -> ApiMethodRow {
let parameters = parameters
.iter()
.map(|parameter_type| ParameterDescriptor {
name: None,
parameter_type: (*parameter_type).to_string(),
})
.collect::<Vec<_>>();
ApiMethodRow {
type_name: type_name.to_string(),
name: name.to_string(),
visibility: visibility.to_string(),
is_static,
is_virtual: false,
is_abstract: false,
return_type: return_type.to_string(),
parameters: parameters.clone(),
signature: format_method_signature(name, return_type, &parameters),
}
}
fn snapshot(types: Vec<ApiTypeRow>, methods: Vec<ApiMethodRow>) -> AssemblyApiSnapshot {
AssemblyApiSnapshot {
assembly: AssemblyDescriptor {
path: PathBuf::from("Fixture.dll"),
assembly_name: "Fixture".to_string(),
assembly_version: Some("1.0.0.0".to_string()),
runtime_version: "v4.0.30319".to_string(),
is_il_only: true,
is_library: true,
is_strong_name_signed: false,
public_key_token: None,
},
types,
methods,
}
}
#[test]
fn diff_snapshots_reports_type_and_method_breaks() {
let old = snapshot(
vec![api_type("Game.Api.OldType", "public", true)],
vec![api_method(
"Game.Api.OldType",
"Launch",
"public",
false,
"void",
&["string"],
)],
);
let new = snapshot(
vec![api_type("Game.Api.NewType", "public", true)],
Vec::new(),
);
let report = diff_api_snapshots(
old,
new,
&ApiDiffQuery {
visibility: ApiVisibilityScope::Public,
include_special: false,
include_missing_method_risks: true,
},
);
assert_eq!(report.summary.removed_types, 1);
assert_eq!(report.summary.added_types, 1);
assert_eq!(report.summary.removed_methods, 1);
assert_eq!(report.summary.missing_method_risks, 1);
assert_eq!(report.removed_types[0].type_name, "Game.Api.OldType");
assert_eq!(report.added_types[0].type_name, "Game.Api.NewType");
assert_eq!(
report.missing_method_risks[0].reason,
"removed_or_changed_public_signature"
);
}
#[test]
fn diff_snapshots_reports_name_group_signature_changes_and_added_overloads() {
let old = snapshot(
vec![api_type("Game.Api.Rocket", "public", true)],
vec![api_method(
"Game.Api.Rocket",
"Launch",
"public",
false,
"void",
&["string"],
)],
);
let new = snapshot(
vec![api_type("Game.Api.Rocket", "public", true)],
vec![
api_method(
"Game.Api.Rocket",
"Launch",
"public",
false,
"void",
&["int"],
),
api_method(
"Game.Api.Rocket",
"Launch",
"public",
false,
"void",
&["int", "bool"],
),
],
);
let report = diff_api_snapshots(old, new, &ApiDiffQuery::default());
assert_eq!(report.summary.signature_changed_methods, 1);
assert_eq!(report.summary.added_methods, 2);
assert_eq!(report.summary.removed_methods, 1);
assert_eq!(
report.signature_changed_methods[0].type_name,
"Game.Api.Rocket"
);
assert_eq!(report.signature_changed_methods[0].method_name, "Launch");
assert_eq!(report.missing_method_risks.len(), 1);
}
#[test]
fn api_visibility_scope_filters_rows() {
let old = snapshot(
vec![
api_type("Game.Api.PublicType", "public", true),
api_type("Game.Api.InternalType", "not_public", false),
api_type("Game.Api.PrivateType", "nested_private", false),
],
vec![
api_method(
"Game.Api.PublicType",
"Public",
"public",
false,
"void",
&[],
),
api_method(
"Game.Api.InternalType",
"Internal",
"assembly",
false,
"void",
&[],
),
api_method(
"Game.Api.PrivateType",
"Private",
"private",
false,
"void",
&[],
),
],
);
let new = snapshot(Vec::new(), Vec::new());
let public = diff_api_snapshots(
old.clone(),
new.clone(),
&ApiDiffQuery {
visibility: ApiVisibilityScope::Public,
..ApiDiffQuery::default()
},
);
let internal = diff_api_snapshots(
old.clone(),
new.clone(),
&ApiDiffQuery {
visibility: ApiVisibilityScope::Internal,
..ApiDiffQuery::default()
},
);
let all = diff_api_snapshots(
old,
new,
&ApiDiffQuery {
visibility: ApiVisibilityScope::All,
..ApiDiffQuery::default()
},
);
assert_eq!(public.summary.removed_methods, 1);
assert_eq!(internal.summary.removed_methods, 2);
assert_eq!(all.summary.removed_methods, 3);
}
#[test]
fn diagnose_dependencies_reports_closure_risks_and_winners() {
let report = diagnose_dependencies(
&[diagnose_fixture_root()],
&DiagnoseQuery {
resolve_dirs: vec![diagnose_server_a_dir(), diagnose_server_b_dir()],
test_only_patterns: Vec::new(),
use_default_test_patterns: true,
},
)
.expect("diagnosis report");
assert_eq!(report.summary.root_count, 1);
assert!(report.summary.error_count > 0);
assert!(report.references.iter().any(|entry| {
entry.reference_name == "MissingOnly"
&& entry.resolution_status == ResolutionStatus::Missing
}));
assert!(
report
.conflicts
.iter()
.any(|entry| entry.reference_name == "RuntimeDependency")
);
assert!(report.winners.iter().any(|entry| {
entry.reference_name == "0Harmony"
&& entry.winner.assembly.assembly_version.as_deref() == Some("2.2.2.0")
}));
assert!(
report
.test_only
.iter()
.any(|entry| entry.assembly.assembly_name == "TestOnlySupport")
);
for expected in [
"missing_reference",
"version_mismatch",
"test_only_dependency",
"missing_method",
"missing_type",
] {
assert!(
report.risks.iter().any(|entry| entry.kind == expected),
"expected risk kind {expected} in {:#?}",
report.risks
);
}
}
#[test]
fn diagnose_dependencies_allows_disabling_default_test_only_patterns() {
let report = diagnose_dependencies(
&[diagnose_fixture_root()],
&DiagnoseQuery {
resolve_dirs: vec![diagnose_server_a_dir(), diagnose_server_b_dir()],
test_only_patterns: Vec::new(),
use_default_test_patterns: false,
},
)
.expect("diagnosis report");
assert!(
!report
.test_only
.iter()
.any(|entry| entry.assembly.assembly_name == "TestOnlySupport")
);
assert!(
!report
.risks
.iter()
.any(|entry| entry.kind == "test_only_dependency")
);
}
#[test]
fn diagnose_dependencies_accepts_explicit_test_only_regex_patterns() {
let report = diagnose_dependencies(
&[diagnose_fixture_root()],
&DiagnoseQuery {
resolve_dirs: vec![diagnose_server_a_dir(), diagnose_server_b_dir()],
test_only_patterns: vec!["TestOnly(Support|Fixture)".to_string()],
use_default_test_patterns: false,
},
)
.expect("diagnosis report");
assert!(
report
.test_only
.iter()
.any(|entry| entry.assembly.assembly_name == "TestOnlySupport")
);
assert!(
report
.risks
.iter()
.any(|entry| entry.kind == "test_only_dependency")
);
}
}