pub mod context;
mod loader;
mod result;
use crate::{
metadata::{
cilobject::CilObject, identity::AssemblyIdentity, tables::TableId, token::Token,
typesystem::CilTypeRc,
},
Error, Result,
};
use dashmap::DashMap;
use std::sync::{Arc, OnceLock};
pub(crate) use context::ProjectContext;
pub use loader::ProjectLoader;
pub use result::{ProjectResult, VersionMismatch};
pub struct CilProject {
assemblies: DashMap<AssemblyIdentity, Arc<CilObject>>,
primary_assembly: OnceLock<Arc<CilObject>>,
}
impl CilProject {
#[must_use]
pub fn new() -> Self {
Self {
assemblies: DashMap::new(),
primary_assembly: OnceLock::new(),
}
}
pub fn get_type_by_name(&self, full_name: &str) -> Option<CilTypeRc> {
let mut typeref_match = None;
for assembly_identity in self.all_assemblies() {
if let Some(assembly) = self.get_assembly(&assembly_identity) {
for entry in assembly.types().iter() {
let type_instance = entry.value();
if type_instance.fullname() == full_name {
if type_instance.token.is_table(TableId::TypeDef) {
return Some(type_instance.clone());
} else if type_instance.token.is_table(TableId::TypeRef) {
typeref_match = Some(type_instance.clone());
}
}
}
}
}
typeref_match
}
pub fn get_types_in_assembly(
&self,
assembly_identity: &AssemblyIdentity,
) -> Vec<(Token, CilTypeRc)> {
if let Some(assembly) = self.get_assembly(assembly_identity) {
assembly
.types()
.iter()
.map(|entry| (*entry.key(), entry.value().clone()))
.collect()
} else {
Vec::new()
}
}
pub fn add_assembly(&self, assembly: CilObject, is_primary: bool) -> Result<()> {
let identity = assembly.identity().ok_or_else(|| {
Error::Configuration("Assembly does not have identity information".to_string())
})?;
if self.assemblies.contains_key(&identity) {
return Err(Error::Configuration(format!(
"Assembly with identity '{}' already exists in project",
identity.name
)));
}
let assembly_arc = Arc::new(assembly);
for existing_entry in &self.assemblies {
let existing_identity = existing_entry.key();
let existing_assembly = existing_entry.value();
assembly_arc
.types()
.registry_link(existing_identity.clone(), existing_assembly.types());
existing_assembly
.types()
.registry_link(identity.clone(), assembly_arc.types());
}
self.assemblies
.insert(identity.clone(), assembly_arc.clone());
if is_primary {
self.primary_assembly
.set(assembly_arc)
.map_err(|existing| {
let existing_name = existing
.identity()
.map_or_else(|| "<unknown>".to_string(), |id| id.name.clone());
Error::Configuration(format!(
"Primary assembly already set to '{}', cannot set '{}' as primary",
existing_name, identity.name
))
})?;
}
Ok(())
}
pub fn get_assembly(&self, identity: &AssemblyIdentity) -> Option<Arc<CilObject>> {
self.assemblies.get(identity).map(|entry| entry.clone())
}
pub fn assembly_count(&self) -> usize {
self.assemblies.len()
}
pub fn all_assemblies(&self) -> Vec<AssemblyIdentity> {
self.assemblies
.iter()
.map(|entry| entry.key().clone())
.collect()
}
pub fn contains_assembly(&self, identity: &AssemblyIdentity) -> bool {
self.assemblies.contains_key(identity)
}
pub fn is_empty(&self) -> bool {
self.assemblies.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (AssemblyIdentity, Arc<CilObject>)> + '_ {
self.assemblies
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
}
pub fn find_type_definitions(&self, type_name: &str) -> Vec<(AssemblyIdentity, CilTypeRc)> {
let mut results = Vec::new();
for (identity, assembly) in self.iter() {
for entry in assembly.types().iter() {
let type_instance = entry.value();
if type_instance.fullname() == type_name
&& type_instance.token.is_table(TableId::TypeDef)
{
results.push((identity.clone(), type_instance.clone()));
}
}
}
results
}
pub fn get_primary(&self) -> Option<Arc<CilObject>> {
self.primary_assembly.get().cloned()
}
pub fn clear(&self) {
self.assemblies.clear();
}
}
impl Default for CilProject {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for CilProject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let primary_identity = self
.primary_assembly
.get()
.and_then(|assembly| assembly.identity());
f.debug_struct("CilProject")
.field("assembly_count", &self.assembly_count())
.field("primary_assembly", &primary_identity)
.field("assemblies", &self.all_assemblies())
.finish()
}
}
#[cfg(test)]
#[cfg_attr(feature = "skip-expensive-tests", allow(unused_imports))]
mod tests {
use crate::test::{verify_crafted_2, verify_windowsbasedll};
use super::*;
#[test]
fn test_cilproject_creation() {
let project = CilProject::new();
assert_eq!(project.assembly_count(), 0);
assert!(project.is_empty());
assert!(project.all_assemblies().is_empty());
assert!(project.get_primary().is_none());
}
#[test]
fn test_cilproject_default() {
let project = CilProject::default();
assert_eq!(project.assembly_count(), 0);
assert!(project.is_empty());
assert!(project.get_primary().is_none());
}
#[test]
#[cfg(not(feature = "skip-expensive-tests"))]
fn test_get_primary_with_loader() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let crafted2_path = std::path::Path::new(&manifest_dir).join("tests/samples/crafted_2.exe");
let mono_deps_path = std::path::Path::new(&manifest_dir).join("tests/samples/mono_4.8");
if !crafted2_path.exists() {
println!(
"Skipping test - crafted_2.exe not found at {:?}",
crafted2_path
);
return;
}
match ProjectLoader::new()
.primary_file(&crafted2_path)
.and_then(|loader| loader.with_search_path(&mono_deps_path))
.and_then(|loader| loader.auto_discover(true).build())
{
Ok(result) => {
if let Some(primary) = result.project.get_primary() {
assert!(
!primary.types().is_empty(),
"Primary assembly should have types"
);
println!("✅ Primary assembly has {} types", primary.types().len());
} else {
panic!("Primary assembly should be set after successful loading");
}
}
Err(e) => {
println!("Skipping test due to loading error: {}", e);
}
}
}
#[test]
#[cfg(not(feature = "skip-expensive-tests"))]
fn test_load_crafted2_exe() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let crafted2_path = std::path::Path::new(&manifest_dir).join("tests/samples/crafted_2.exe");
let mono_deps_path = std::path::Path::new(&manifest_dir).join("tests/samples/mono_4.8");
println!("Loading crafted_2.exe from {:?}", crafted2_path);
println!("Using Mono dependencies from {:?}", mono_deps_path);
match ProjectLoader::new()
.primary_file(&crafted2_path)
.and_then(|loader| loader.with_search_path(&mono_deps_path))
.map(|loader| loader.auto_discover(true))
.and_then(|loader| loader.strict_mode(true).build())
{
Ok(result) => {
println!(
"Loaded: {}, Failed: {}",
result.success_count(),
result.failure_count()
);
println!("Loaded assemblies:");
for identity in &result.loaded_assemblies {
println!(" - {} v{}", identity.name, identity.version);
}
if !result.failed_loads.is_empty() {
println!("Failed to load {} assemblies:", result.failed_loads.len());
for (path, error) in &result.failed_loads {
println!(" - {}: {}", path, error);
}
}
if !result.missing_dependencies.is_empty() {
println!("Missing dependencies:");
for dep in &result.missing_dependencies {
println!(" - {}", dep);
}
}
let crafted2_loaded = result
.loaded_assemblies
.iter()
.any(|identity| identity.name == "crafted_2");
assert!(crafted2_loaded,
"crafted_2.exe (the root assembly) must be loaded successfully. Loaded assemblies: {:?}",
result.loaded_assemblies.iter().map(|id| &id.name).collect::<Vec<_>>());
let loaded = result.project.get_primary().unwrap();
verify_crafted_2(&loaded);
}
Err(e) => {
panic!(
"❌ Test FAILED: Assembly loading must succeed without errors, but got: {}",
e
);
}
}
}
#[test]
#[cfg(not(feature = "skip-expensive-tests"))]
fn test_load_windowsbase_dll() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let windowsbase_path =
std::path::Path::new(&manifest_dir).join("tests/samples/WindowsBase.dll");
let mono_deps_path = std::path::Path::new(&manifest_dir).join("tests/samples/mono_4.8");
println!("Loading WindowsBase.dll from {:?}", windowsbase_path);
println!("Using Mono dependencies from {:?}", mono_deps_path);
match ProjectLoader::new()
.primary_file(&windowsbase_path)
.and_then(|loader| loader.with_search_path(&mono_deps_path))
.map(|loader| loader.auto_discover(true))
.and_then(|loader| loader.strict_mode(true).build())
{
Ok(result) => {
println!(
"Loaded: {}, Failed: {}",
result.success_count(),
result.failure_count()
);
println!("Loaded assemblies:");
for identity in &result.loaded_assemblies {
println!(" - {} v{}", identity.name, identity.version);
}
if !result.failed_loads.is_empty() {
println!("Failed to load {} assemblies:", result.failed_loads.len());
for (path, error) in &result.failed_loads {
println!(" - {}: {}", path, error);
}
}
if !result.missing_dependencies.is_empty() {
println!("Missing dependencies:");
for dep in &result.missing_dependencies {
println!(" - {}", dep);
}
}
let windowsbase_loaded = result
.loaded_assemblies
.iter()
.any(|identity| identity.name == "WindowsBase");
assert!(windowsbase_loaded,
"WindowsBase.dll (the root assembly) must be loaded successfully. Loaded assemblies: {:?}",
result.loaded_assemblies.iter().map(|id| &id.name).collect::<Vec<_>>());
assert!(result.success_count() >= 1);
assert!(!result.loaded_assemblies.is_empty());
let loaded = result.project.get_primary().unwrap();
verify_windowsbasedll(&loaded)
}
Err(e) => {
panic!("❌ Failed to load WindowsBase.dll: {:?}", e);
}
}
}
}