use std::sync::Arc;
use crate::{
metadata::{
dependencies::{
AssemblyDependency, AssemblyDependencyGraph, DependencyResolutionState,
DependencySource, DependencyType, VersionRequirement,
},
identity::{AssemblyIdentity, AssemblyVersion, Identity, ProcessorArchitecture},
loader::LoaderContext,
tables::{AssemblyRefRaw, File, FileRaw, ModuleRef, ModuleRefRaw},
},
Error, Result,
};
pub(crate) fn perform_dependency_analysis(
context: &LoaderContext,
dependency_graph: Arc<AssemblyDependencyGraph>,
) -> Result<()> {
let analyzer = DependencyAnalyzer::new(dependency_graph);
analyzer.analyze_all_dependencies(context)
}
pub struct DependencyAnalyzer {
graph: Arc<AssemblyDependencyGraph>,
}
impl DependencyAnalyzer {
#[must_use]
pub fn new(graph: Arc<AssemblyDependencyGraph>) -> Self {
Self { graph }
}
pub(crate) fn analyze_assembly_references(&self, context: &LoaderContext) -> Result<()> {
if let (Some(header), Some(strings), Some(blobs)) =
(context.meta, context.strings, context.blobs)
{
if let Some(table) = header.table::<AssemblyRefRaw>() {
let source_identity = Self::extract_current_assembly_identity(context)?;
for row in table {
let assembly_ref = row.to_owned(strings, blobs)?;
let target_identity = AssemblyIdentity::from_assembly_ref(&assembly_ref);
let dependency = AssemblyDependency {
source: DependencySource::AssemblyRef(assembly_ref),
target_identity,
dependency_type: DependencyType::Reference, version_requirement: VersionRequirement::Compatible, is_optional: false, resolution_state: DependencyResolutionState::Unresolved, };
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
}
}
Ok(())
}
pub(crate) fn analyze_module_references(&self, context: &LoaderContext) -> Result<()> {
if let (Some(header), Some(strings)) = (context.meta, context.strings) {
if let Some(table) = header.table::<ModuleRefRaw>() {
let source_identity = Self::extract_current_assembly_identity(context)?;
for row in table {
let module_ref = row.to_owned(strings)?;
let module_classification = Self::classify_module_ref(&module_ref);
match module_classification {
ModuleRefType::NativeLibrary => {
}
ModuleRefType::NetModule => {
let target_identity =
Self::create_module_assembly_identity(&module_ref);
let dependency = AssemblyDependency {
source: DependencySource::ModuleRef(module_ref),
target_identity,
dependency_type: DependencyType::Reference,
version_requirement: VersionRequirement::Compatible,
is_optional: false,
resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
ModuleRefType::ExternalAssemblyModule => {
let target_identity =
Self::create_module_assembly_identity(&module_ref);
let dependency = AssemblyDependency {
source: DependencySource::ModuleRef(module_ref),
target_identity,
dependency_type: DependencyType::Reference,
version_requirement: VersionRequirement::Compatible,
is_optional: false,
resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
ModuleRefType::Unknown => {
let target_identity =
Self::create_module_assembly_identity(&module_ref);
let dependency = AssemblyDependency {
source: DependencySource::ModuleRef(module_ref),
target_identity,
dependency_type: DependencyType::Reference,
version_requirement: VersionRequirement::Any, is_optional: true, resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
}
}
}
}
Ok(())
}
pub(crate) fn analyze_file_references(&self, context: &LoaderContext) -> Result<()> {
if let (Some(header), Some(strings), Some(blobs)) =
(context.meta, context.strings, context.blobs)
{
if let Some(table) = header.table::<FileRaw>() {
let source_identity = Self::extract_current_assembly_identity(context)?;
for row in table {
let file_ref = row.to_owned(blobs, strings)?;
let file_classification = Self::classify_file_ref(&file_ref);
match file_classification {
FileRefType::IntraAssemblyModule => {
let target_identity = Self::create_file_assembly_identity(&file_ref);
let dependency = AssemblyDependency {
source: DependencySource::File(file_ref),
target_identity,
dependency_type: DependencyType::Reference,
version_requirement: VersionRequirement::Exact, is_optional: false,
resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
FileRefType::ResourceFile => {
let target_identity = Self::create_file_assembly_identity(&file_ref);
let dependency = AssemblyDependency {
source: DependencySource::File(file_ref),
target_identity,
dependency_type: DependencyType::Resource,
version_requirement: VersionRequirement::Compatible,
is_optional: true, resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
FileRefType::ExternalAssemblyFile => {
let target_identity = Self::create_file_assembly_identity(&file_ref);
let dependency = AssemblyDependency {
source: DependencySource::File(file_ref),
target_identity,
dependency_type: DependencyType::Reference,
version_requirement: VersionRequirement::Compatible,
is_optional: false,
resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
FileRefType::DocumentationFile => {
}
FileRefType::Unknown => {
let target_identity = Self::create_file_assembly_identity(&file_ref);
let dependency = AssemblyDependency {
source: DependencySource::File(file_ref),
target_identity,
dependency_type: DependencyType::Reference,
version_requirement: VersionRequirement::Any,
is_optional: true, resolution_state: DependencyResolutionState::Unresolved,
};
self.graph
.add_dependency_with_source(&source_identity, dependency)?;
}
}
}
}
}
Ok(())
}
#[must_use]
pub fn dependency_graph(&self) -> &Arc<AssemblyDependencyGraph> {
&self.graph
}
pub(crate) fn analyze_all_dependencies(&self, context: &LoaderContext) -> Result<()> {
self.analyze_assembly_references(context)?;
self.analyze_module_references(context)?;
self.analyze_file_references(context)?;
Ok(())
}
fn classify_module_ref(module_ref: &ModuleRef) -> ModuleRefType {
let name = &module_ref.name;
if Self::is_native_library(name) {
return ModuleRefType::NativeLibrary;
}
if name.ends_with(".netmodule") {
return ModuleRefType::NetModule;
}
if std::path::Path::new(name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("exe"))
{
return ModuleRefType::ExternalAssemblyModule;
}
ModuleRefType::Unknown
}
fn is_native_library(name: &str) -> bool {
const NATIVE_LIBRARIES: &[&str] = &[
"kernel32.dll",
"user32.dll",
"gdi32.dll",
"advapi32.dll",
"ole32.dll",
"oleaut32.dll",
"shell32.dll",
"comdlg32.dll",
"comctl32.dll",
"winmm.dll",
"msvcrt.dll",
"ntdll.dll",
"ws2_32.dll",
"wininet.dll",
"crypt32.dll",
"version.dll",
"psapi.dll",
"dbghelp.dll",
"imagehlp.dll",
"userenv.dll",
];
const NATIVE_PREFIXES: &[&str] = &["msvcr", "msvcp", "vcruntime", "api-ms-", "ext-ms-"];
let lower_name = name.to_lowercase();
if NATIVE_LIBRARIES.iter().any(|&lib| lower_name == lib) {
return true;
}
if NATIVE_PREFIXES
.iter()
.any(|&prefix| lower_name.starts_with(prefix))
{
return true;
}
false
}
fn create_module_assembly_identity(module_ref: &ModuleRef) -> AssemblyIdentity {
let assembly_name = if let Some(name_without_ext) = module_ref.name.strip_suffix(".dll") {
name_without_ext.to_string()
} else if let Some(name_without_ext) = module_ref.name.strip_suffix(".exe") {
name_without_ext.to_string()
} else if let Some(name_without_ext) = module_ref.name.strip_suffix(".netmodule") {
name_without_ext.to_string()
} else {
module_ref.name.clone()
};
AssemblyIdentity {
name: assembly_name,
version: AssemblyVersion::UNKNOWN,
culture: None,
strong_name: None,
processor_architecture: None,
}
}
fn classify_file_ref(file_ref: &File) -> FileRefType {
const CONTAINS_META_DATA: u32 = 0x0000;
const CONTAINS_NO_META_DATA: u32 = 0x0001;
let name = &file_ref.name;
let flags = file_ref.flags;
let path = std::path::Path::new(name);
if flags == CONTAINS_META_DATA && name.ends_with(".netmodule") {
return FileRefType::IntraAssemblyModule;
}
let Some(extension) = path.extension() else {
return FileRefType::Unknown;
};
if flags == CONTAINS_NO_META_DATA
&& (extension.eq_ignore_ascii_case("resources")
|| extension.eq_ignore_ascii_case("resx"))
{
return FileRefType::ResourceFile;
}
if extension.eq_ignore_ascii_case("xml")
|| extension.eq_ignore_ascii_case("pdb")
|| extension.eq_ignore_ascii_case("mdb")
{
return FileRefType::DocumentationFile;
}
if extension.eq_ignore_ascii_case("dll") || extension.eq_ignore_ascii_case("exe") {
return FileRefType::ExternalAssemblyFile;
}
FileRefType::Unknown
}
fn create_file_assembly_identity(file_ref: &File) -> AssemblyIdentity {
let assembly_name = if let Some(name_without_ext) = file_ref.name.strip_suffix(".dll") {
name_without_ext.to_string()
} else if let Some(name_without_ext) = file_ref.name.strip_suffix(".exe") {
name_without_ext.to_string()
} else if let Some(name_without_ext) = file_ref.name.strip_suffix(".netmodule") {
name_without_ext.to_string()
} else if let Some(name_without_ext) = file_ref.name.strip_suffix(".resources") {
name_without_ext.to_string()
} else {
file_ref.name.clone()
};
AssemblyIdentity {
name: assembly_name,
version: AssemblyVersion::UNKNOWN,
culture: None,
strong_name: None,
processor_architecture: None,
}
}
fn extract_current_assembly_identity(context: &LoaderContext) -> Result<AssemblyIdentity> {
let assembly_lock = context.assembly.get().ok_or_else(|| {
Error::TypeError(
"Cannot extract assembly identity: assembly metadata not yet loaded. \
Dependency analysis requires the Assembly table to be loaded first."
.to_string(),
)
})?;
let strong_name = if let Some(ref public_key_data) = assembly_lock.public_key {
Some(Identity::from(public_key_data, true)?)
} else {
None
};
#[allow(clippy::cast_possible_truncation)]
Ok(AssemblyIdentity {
name: assembly_lock.name.clone(),
version: AssemblyVersion::new(
assembly_lock.major_version as u16,
assembly_lock.minor_version as u16,
assembly_lock.build_number as u16,
assembly_lock.revision_number as u16,
),
culture: assembly_lock.culture.clone(),
strong_name,
processor_architecture: context
.assembly_processor
.get()
.and_then(|proc| ProcessorArchitecture::try_from(proc.processor).ok()),
})
}
}
#[derive(Debug, Clone, PartialEq)]
enum ModuleRefType {
NativeLibrary,
NetModule,
ExternalAssemblyModule,
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
enum FileRefType {
IntraAssemblyModule,
ResourceFile,
ExternalAssemblyFile,
DocumentationFile,
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::helpers::dependencies::{create_test_file, create_test_module_ref};
fn create_test_analyzer() -> DependencyAnalyzer {
let graph = Arc::new(AssemblyDependencyGraph::new());
DependencyAnalyzer::new(graph)
}
#[test]
fn test_dependency_analyzer_creation() {
let graph = Arc::new(AssemblyDependencyGraph::new());
let analyzer = DependencyAnalyzer::new(graph.clone());
assert_eq!(analyzer.dependency_graph().assembly_count(), 0);
}
#[test]
fn test_classify_module_ref_native_library() {
let kernel32 = create_test_module_ref("kernel32.dll");
let classification = DependencyAnalyzer::classify_module_ref(&kernel32);
assert_eq!(classification, ModuleRefType::NativeLibrary);
let user32 = create_test_module_ref("user32.dll");
let classification = DependencyAnalyzer::classify_module_ref(&user32);
assert_eq!(classification, ModuleRefType::NativeLibrary);
let msvcrt = create_test_module_ref("msvcrt.dll");
let classification = DependencyAnalyzer::classify_module_ref(&msvcrt);
assert_eq!(classification, ModuleRefType::NativeLibrary);
}
#[test]
fn test_classify_module_ref_net_module() {
let netmodule = create_test_module_ref("MyModule.netmodule");
let classification = DependencyAnalyzer::classify_module_ref(&netmodule);
assert_eq!(classification, ModuleRefType::NetModule);
}
#[test]
fn test_classify_module_ref_external_assembly() {
let dll_module = create_test_module_ref("ExternalLibrary.dll");
let classification = DependencyAnalyzer::classify_module_ref(&dll_module);
assert_eq!(classification, ModuleRefType::ExternalAssemblyModule);
let exe_module = create_test_module_ref("Application.exe");
let classification = DependencyAnalyzer::classify_module_ref(&exe_module);
assert_eq!(classification, ModuleRefType::ExternalAssemblyModule);
}
#[test]
fn test_classify_module_ref_unknown() {
let unknown_module = create_test_module_ref("SomeFile.unknown");
let classification = DependencyAnalyzer::classify_module_ref(&unknown_module);
assert_eq!(classification, ModuleRefType::Unknown);
}
#[test]
fn test_is_native_library() {
assert!(DependencyAnalyzer::is_native_library("kernel32.dll"));
assert!(DependencyAnalyzer::is_native_library("KERNEL32.DLL")); assert!(DependencyAnalyzer::is_native_library("user32.dll"));
assert!(DependencyAnalyzer::is_native_library("msvcrt.dll"));
assert!(DependencyAnalyzer::is_native_library("msvcr120.dll"));
assert!(DependencyAnalyzer::is_native_library("msvcp140.dll"));
assert!(DependencyAnalyzer::is_native_library("vcruntime140.dll"));
assert!(DependencyAnalyzer::is_native_library(
"api-ms-win-core-kernel32-l1-1-0.dll"
));
assert!(!DependencyAnalyzer::is_native_library("System.dll"));
assert!(!DependencyAnalyzer::is_native_library("MyAssembly.dll"));
assert!(!DependencyAnalyzer::is_native_library("Module.netmodule"));
}
#[test]
fn test_create_module_assembly_identity() {
let dll_module = create_test_module_ref("TestLibrary.dll");
let identity = DependencyAnalyzer::create_module_assembly_identity(&dll_module);
assert_eq!(identity.name, "TestLibrary");
assert_eq!(identity.version, AssemblyVersion::UNKNOWN);
assert!(identity.version.is_unknown());
let exe_module = create_test_module_ref("Application.exe");
let identity = DependencyAnalyzer::create_module_assembly_identity(&exe_module);
assert_eq!(identity.name, "Application");
let netmodule = create_test_module_ref("Module.netmodule");
let identity = DependencyAnalyzer::create_module_assembly_identity(&netmodule);
assert_eq!(identity.name, "Module");
let no_ext_module = create_test_module_ref("SomeModule");
let identity = DependencyAnalyzer::create_module_assembly_identity(&no_ext_module);
assert_eq!(identity.name, "SomeModule");
}
#[test]
fn test_classify_file_ref_intra_assembly_module() {
let mut file = create_test_file("Module.netmodule");
let file_mut = Arc::get_mut(&mut file).unwrap();
file_mut.flags = 0x0000;
let classification = DependencyAnalyzer::classify_file_ref(&file);
assert_eq!(classification, FileRefType::IntraAssemblyModule);
}
#[test]
fn test_classify_file_ref_resource_file() {
let mut resources_file = create_test_file("Strings.resources");
let file_mut = Arc::get_mut(&mut resources_file).unwrap();
file_mut.flags = 0x0001;
let classification = DependencyAnalyzer::classify_file_ref(&resources_file);
assert_eq!(classification, FileRefType::ResourceFile);
let mut resx_file = create_test_file("Form.resx");
let file_mut = Arc::get_mut(&mut resx_file).unwrap();
file_mut.flags = 0x0001;
let classification = DependencyAnalyzer::classify_file_ref(&resx_file);
assert_eq!(classification, FileRefType::ResourceFile);
}
#[test]
fn test_classify_file_ref_documentation_file() {
let xml_file = create_test_file("Documentation.xml");
let classification = DependencyAnalyzer::classify_file_ref(&xml_file);
assert_eq!(classification, FileRefType::DocumentationFile);
let pdb_file = create_test_file("Debug.pdb");
let classification = DependencyAnalyzer::classify_file_ref(&pdb_file);
assert_eq!(classification, FileRefType::DocumentationFile);
let mdb_file = create_test_file("Debug.mdb");
let classification = DependencyAnalyzer::classify_file_ref(&mdb_file);
assert_eq!(classification, FileRefType::DocumentationFile);
}
#[test]
fn test_classify_file_ref_external_assembly_file() {
let dll_file = create_test_file("External.dll");
let classification = DependencyAnalyzer::classify_file_ref(&dll_file);
assert_eq!(classification, FileRefType::ExternalAssemblyFile);
let exe_file = create_test_file("Application.exe");
let classification = DependencyAnalyzer::classify_file_ref(&exe_file);
assert_eq!(classification, FileRefType::ExternalAssemblyFile);
}
#[test]
fn test_classify_file_ref_unknown() {
let unknown_file = create_test_file("SomeFile.unknown");
let classification = DependencyAnalyzer::classify_file_ref(&unknown_file);
assert_eq!(classification, FileRefType::Unknown);
}
#[test]
fn test_create_file_assembly_identity() {
let dll_file = create_test_file("TestLibrary.dll");
let identity = DependencyAnalyzer::create_file_assembly_identity(&dll_file);
assert_eq!(identity.name, "TestLibrary");
assert_eq!(identity.version, AssemblyVersion::UNKNOWN);
assert!(identity.version.is_unknown());
let exe_file = create_test_file("Application.exe");
let identity = DependencyAnalyzer::create_file_assembly_identity(&exe_file);
assert_eq!(identity.name, "Application");
let netmodule_file = create_test_file("Module.netmodule");
let identity = DependencyAnalyzer::create_file_assembly_identity(&netmodule_file);
assert_eq!(identity.name, "Module");
let resources_file = create_test_file("Strings.resources");
let identity = DependencyAnalyzer::create_file_assembly_identity(&resources_file);
assert_eq!(identity.name, "Strings");
let no_ext_file = create_test_file("SomeFile");
let identity = DependencyAnalyzer::create_file_assembly_identity(&no_ext_file);
assert_eq!(identity.name, "SomeFile");
}
}