2501 lines
79 KiB
Rust
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, ¶meters);
|
|
|
|
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,
|
|
¶meters,
|
|
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, ¶meters)
|
|
}
|
|
|
|
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", ¶meters),
|
|
"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, ¶meters),
|
|
}
|
|
}
|
|
|
|
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")
|
|
);
|
|
}
|
|
}
|