use crate::prelude::*;
use crate::test::mono::capabilities::{Disassembler, TestCapabilities};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct DisassemblyResult {
pub success: bool,
pub il_output: String,
pub error: Option<String>,
pub disassembler: Option<Disassembler>,
}
impl DisassemblyResult {
pub fn success(il_output: String, disassembler: Disassembler) -> Self {
Self {
success: true,
il_output,
error: None,
disassembler: Some(disassembler),
}
}
pub fn failure(error: String) -> Self {
Self {
success: false,
il_output: String::new(),
error: Some(error),
disassembler: None,
}
}
pub fn is_success(&self) -> bool {
self.success
}
pub fn is_framework_error(&self) -> bool {
if let Some(ref error) = self.error {
error.contains("You must install or update .NET to run this application")
|| error.contains("Framework:")
&& error.contains("version")
&& error.contains("was not found")
|| error.contains("The framework 'Microsoft.NETCore.App'")
} else {
false
}
}
pub fn contains_method(&self, method_name: &str) -> bool {
self.il_output.contains(&format!(" {} ", method_name))
|| self.il_output.contains(&format!(" {}(", method_name))
|| self.il_output.contains(".method") && self.il_output.contains(method_name)
}
pub fn contains_class(&self, class_name: &str) -> bool {
self.il_output.contains(".class") && self.il_output.contains(class_name)
}
pub fn contains_instruction(&self, instruction: &str) -> bool {
self.il_output.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with(instruction) || trimmed.contains(&format!(" {} ", instruction))
})
}
pub fn method_names(&self) -> Vec<String> {
self.il_output
.lines()
.filter(|line| line.contains(".method"))
.filter_map(|line| {
if let Some(paren_pos) = line.find('(') {
let before_paren = &line[..paren_pos];
before_paren
.split_whitespace()
.last()
.map(|s| s.to_string())
} else {
None
}
})
.collect()
}
}
pub fn disassemble(
capabilities: &TestCapabilities,
assembly_path: &Path,
) -> Result<DisassemblyResult> {
let disassembler = match capabilities.disassembler {
Some(d) => d,
None => {
return Ok(DisassemblyResult::failure(
"No IL disassembler available".to_string(),
))
}
};
disassemble_with_caps(
disassembler,
assembly_path,
capabilities.ildasm_path.as_deref(),
)
}
pub fn disassemble_with(
disassembler: Disassembler,
assembly_path: &Path,
) -> Result<DisassemblyResult> {
disassemble_with_caps(disassembler, assembly_path, None)
}
fn disassemble_with_caps(
disassembler: Disassembler,
assembly_path: &Path,
ildasm_path: Option<&Path>,
) -> Result<DisassemblyResult> {
let output = match disassembler {
Disassembler::Monodis => disassemble_with_monodis(assembly_path)?,
Disassembler::Ildasm => disassemble_with_ildasm(assembly_path, ildasm_path)?,
Disassembler::DotNetIldasm => disassemble_with_dotnet_ildasm(assembly_path)?,
};
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if output.status.success() {
Ok(DisassemblyResult::success(stdout, disassembler))
} else {
let error = if !stderr.is_empty() {
stderr
} else if !stdout.is_empty() {
stdout
} else {
"Disassembly failed with unknown error".to_string()
};
Ok(DisassemblyResult::failure(error))
}
}
fn disassemble_with_monodis(assembly_path: &Path) -> Result<std::process::Output> {
Command::new("monodis")
.arg(assembly_path)
.output()
.map_err(|e| Error::Other(format!("Failed to execute monodis: {}", e)))
}
fn disassemble_with_ildasm(
assembly_path: &Path,
ildasm_path: Option<&Path>,
) -> Result<std::process::Output> {
let cmd = ildasm_path
.map(|p| p.as_os_str().to_os_string())
.unwrap_or_else(|| std::ffi::OsString::from("ildasm"));
Command::new(cmd)
.arg("/text")
.arg("/nobar")
.arg(assembly_path)
.output()
.map_err(|e| Error::Other(format!("Failed to execute ildasm: {}", e)))
}
fn disassemble_with_dotnet_ildasm(assembly_path: &Path) -> Result<std::process::Output> {
Command::new("dotnet-ildasm")
.arg(assembly_path)
.output()
.map_err(|e| Error::Other(format!("Failed to execute dotnet-ildasm: {}", e)))
}
#[derive(Debug)]
pub struct VerificationResult {
pub success: bool,
pub checks: Vec<(String, bool)>,
pub disassembly: DisassemblyResult,
}
impl VerificationResult {
pub fn is_success(&self) -> bool {
self.success
}
pub fn failed_checks(&self) -> Vec<&str> {
self.checks
.iter()
.filter(|(_, passed)| !passed)
.map(|(name, _)| name.as_str())
.collect()
}
}
pub fn verify(
capabilities: &TestCapabilities,
assembly_path: &Path,
expected_methods: &[&str],
expected_classes: &[&str],
) -> Result<VerificationResult> {
let disassembly = disassemble(capabilities, assembly_path)?;
if !disassembly.is_success() {
return Ok(VerificationResult {
success: false,
checks: vec![("disassembly".to_string(), false)],
disassembly,
});
}
let mut checks = Vec::new();
let mut all_passed = true;
for method in expected_methods {
let found = disassembly.contains_method(method);
checks.push((format!("method:{}", method), found));
if !found {
all_passed = false;
}
}
for class in expected_classes {
let found = disassembly.contains_class(class);
checks.push((format!("class:{}", class), found));
if !found {
all_passed = false;
}
}
Ok(VerificationResult {
success: all_passed,
checks,
disassembly,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::mono::compilation::{compile, templates};
use tempfile::TempDir;
#[test]
fn test_disassembly_result() {
let success = DisassemblyResult::success(
".method public static void Main()".to_string(),
Disassembler::Monodis,
);
assert!(success.is_success());
assert!(success.contains_method("Main"));
let failure = DisassemblyResult::failure("Error".to_string());
assert!(!failure.is_success());
}
#[test]
fn test_disassemble_assembly() -> Result<()> {
let caps = TestCapabilities::detect();
if !caps.can_test() || !caps.can_disassemble() {
println!("Skipping: no compiler/disassembler available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let arch = caps.supported_architectures.first().unwrap();
let compile_result = compile(
&caps,
templates::SIMPLE_CLASS,
temp_dir.path(),
"test",
arch,
)?;
assert!(compile_result.is_success(), "Compilation failed");
let disasm_result = disassemble(&caps, compile_result.assembly_path())?;
assert!(
disasm_result.is_success(),
"Disassembly failed: {:?}",
disasm_result.error
);
assert!(disasm_result.contains_method("Main"));
assert!(disasm_result.contains_method("Add"));
assert!(disasm_result.contains_class("TestClass"));
Ok(())
}
}