use xll_utils::exports::{export_diff, verify_dll_exports, verify_dll_exports_strict, verify_pe_exports};
use xll_utils::pe::{parse_pe_bytes, parse_pe_file};
use xll_utils::types::{Architecture, ExportInfo, PeFile};
use xll_utils::xll::{is_valid_xll, verify_xll_entry_points, xll_info};
fn fixture_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("test_xll.xll")
}
#[test]
fn full_workflow_parse_verify_xll_info() {
let path = fixture_path();
let pe = parse_pe_file(&path).unwrap();
assert!(pe.is_dll());
assert_eq!(pe.architecture(), Architecture::X64);
let expected = &[
"xlAutoOpen",
"xlAutoClose",
"xlAutoFree12",
"xlAddInManagerInfo12",
"xl_add",
"xl_multiply",
"xl_negate",
"xl_version",
];
let report = verify_pe_exports(&pe, expected);
assert!(report.complete, "missing: {:?}", report.missing);
assert_eq!(report.found.len(), expected.len());
assert!(verify_xll_entry_points(&pe));
let info = xll_info(&path).unwrap();
assert_eq!(info.name, "test_xll");
assert!(info.has_auto_open);
assert!(info.has_auto_free);
assert_eq!(info.architecture, pe.architecture());
assert_eq!(info.export_count, pe.exports().len());
}
#[test]
fn workflow_verify_then_strict_verify() {
let path = fixture_path();
let expected = &["xlAutoOpen", "xlAutoClose"];
let report = verify_dll_exports(&path, expected).unwrap();
assert!(report.complete);
let strict = verify_dll_exports_strict(&path, expected, false).unwrap();
assert!(strict.complete);
assert!(strict.mismatches.is_empty());
assert_eq!(report.unexpected.len(), strict.unexpected.len());
}
#[test]
fn workflow_diff_file_against_itself() {
let pe = parse_pe_file(fixture_path()).unwrap();
let names = pe.export_names();
let expected: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
let diff = export_diff(&expected, pe.exports());
assert!(diff.missing.is_empty(), "diffing against self should have no missing");
assert!(diff.extra.is_empty(), "diffing against self should have no extra");
assert_eq!(diff.common.len(), names.len());
}
#[test]
fn workflow_parse_bytes_then_verify() {
let data = std::fs::read(fixture_path()).unwrap();
let pe = parse_pe_bytes(&data).unwrap();
let report = verify_pe_exports(&pe, &["xlAutoOpen", "xlAutoFree12"]);
assert!(report.complete);
assert_eq!(report.dll_path, std::path::PathBuf::new());
}
#[test]
fn xll_info_consistent_with_pe() {
let path = fixture_path();
let pe = parse_pe_file(&path).unwrap();
let info = xll_info(&path).unwrap();
assert_eq!(info.export_count, pe.exports().len());
assert_eq!(info.exports, pe.export_names());
assert_eq!(info.architecture, pe.architecture());
assert_eq!(info.has_auto_open, pe.find_export("xlAutoOpen").is_some());
assert_eq!(info.has_auto_close, pe.find_export("xlAutoClose").is_some());
assert_eq!(info.has_auto_free, pe.find_export("xlAutoFree12").is_some());
assert_eq!(
info.has_addin_manager_info,
pe.find_export("xlAddInManagerInfo12").is_some()
);
}
fn make_pe_file(export_names: &[&str]) -> PeFile {
let exports: Vec<ExportInfo> = export_names
.iter()
.enumerate()
.map(|(i, name)| ExportInfo {
name: Some(name.to_string()),
ordinal: i as u32 + 1,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x1000 + i as u32 * 0x100),
})
.collect();
PeFile {
path: std::path::PathBuf::from("synthetic.xll"),
architecture: Architecture::X64,
is_dll: true,
image_base: 0x180000000,
entry_point_rva: 0x1000,
export_directory: None,
exports,
}
}
#[test]
fn entry_points_missing_auto_open() {
let pe = make_pe_file(&["xlAutoFree12", "xlAutoClose", "myFunc"]);
assert!(!verify_xll_entry_points(&pe), "should fail without xlAutoOpen");
}
#[test]
fn entry_points_missing_auto_free() {
let pe = make_pe_file(&["xlAutoOpen", "xlAutoClose", "myFunc"]);
assert!(!verify_xll_entry_points(&pe), "should fail without xlAutoFree12");
}
#[test]
fn entry_points_missing_both() {
let pe = make_pe_file(&["xlAutoClose", "myFunc"]);
assert!(!verify_xll_entry_points(&pe));
}
#[test]
fn entry_points_no_exports() {
let pe = make_pe_file(&[]);
assert!(!verify_xll_entry_points(&pe));
}
#[test]
fn entry_points_valid_minimal() {
let pe = make_pe_file(&["xlAutoOpen", "xlAutoFree12"]);
assert!(verify_xll_entry_points(&pe));
}
#[test]
fn verify_pe_exports_duplicate_expected_names() {
let pe = make_pe_file(&["foo", "bar"]);
let report = verify_pe_exports(&pe, &["foo", "foo"]);
assert!(report.complete);
assert_eq!(report.found, vec!["foo".to_string(), "foo".to_string()]);
}
#[test]
fn verify_pe_exports_no_exports_in_pe() {
let pe = make_pe_file(&[]);
let report = verify_pe_exports(&pe, &["foo"]);
assert!(!report.complete);
assert_eq!(report.missing, vec!["foo".to_string()]);
assert!(report.unexpected.is_empty());
assert_eq!(report.total_exports, 0);
}
#[test]
fn verify_pe_exports_no_expected() {
let pe = make_pe_file(&["foo", "bar"]);
let report = verify_pe_exports(&pe, &[]);
assert!(report.complete);
assert_eq!(report.unexpected, vec!["bar".to_string(), "foo".to_string()]);
}
#[test]
fn diff_completely_disjoint() {
let actual = vec![
ExportInfo {
name: Some("a".into()),
ordinal: 1,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x1000),
},
ExportInfo {
name: Some("b".into()),
ordinal: 2,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x2000),
},
];
let diff = export_diff(&["x", "y"], &actual);
assert_eq!(diff.missing, vec!["x".to_string(), "y".to_string()]);
assert_eq!(diff.extra, vec!["a".to_string(), "b".to_string()]);
assert!(diff.common.is_empty());
}
#[test]
fn diff_identical_lists() {
let actual = vec![
ExportInfo {
name: Some("foo".into()),
ordinal: 1,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x1000),
},
];
let diff = export_diff(&["foo"], &actual);
assert!(diff.missing.is_empty());
assert!(diff.extra.is_empty());
assert_eq!(diff.common, vec!["foo".to_string()]);
}
#[test]
fn diff_ordinal_only_exports_ignored() {
let actual = vec![
ExportInfo {
name: None,
ordinal: 1,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x1000),
},
ExportInfo {
name: Some("named".into()),
ordinal: 2,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x2000),
},
];
let diff = export_diff(&["named"], &actual);
assert!(diff.missing.is_empty());
assert!(diff.extra.is_empty());
assert_eq!(diff.common, vec!["named".to_string()]);
}
#[test]
fn parse_empty_data() {
let err = parse_pe_bytes(&[]).unwrap_err();
assert!(matches!(err, xll_utils::Error::InvalidFileFormat));
}
#[test]
fn parse_truncated_mz_header() {
let err = parse_pe_bytes(b"MZ").unwrap_err();
assert!(matches!(err, xll_utils::Error::InvalidFileFormat));
}
#[test]
fn parse_random_bytes() {
let data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22, 0x33];
let err = parse_pe_bytes(&data).unwrap_err();
assert!(matches!(err, xll_utils::Error::InvalidFileFormat));
}
#[test]
fn is_valid_xll_invalid_file_data() {
let dir = std::env::temp_dir().join("xll_utils_test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("not_a_pe.xll");
std::fs::write(&path, b"this is not a PE file").unwrap();
let result = is_valid_xll(&path);
assert!(result.is_err());
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(&dir);
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
#[test]
fn architecture_serde_roundtrip() {
for arch in [
Architecture::X86,
Architecture::X64,
Architecture::Arm,
Architecture::Arm64,
Architecture::Unknown(0x1234),
] {
let json = serde_json::to_string(&arch).unwrap();
let back: Architecture = serde_json::from_str(&json).unwrap();
assert_eq!(arch, back);
}
}
#[test]
fn export_info_serde_roundtrip() {
let info = ExportInfo {
name: Some("xlAutoOpen".into()),
ordinal: 1,
is_forwarded: false,
forward_to: None,
relative_address: Some(0x1234),
};
let json = serde_json::to_string(&info).unwrap();
let back: ExportInfo = serde_json::from_str(&json).unwrap();
assert_eq!(info, back);
}
#[test]
fn export_info_forwarded_serde_roundtrip() {
let info = ExportInfo {
name: Some("ForwardedFunc".into()),
ordinal: 42,
is_forwarded: true,
forward_to: Some("OTHER.dll.Func".into()),
relative_address: None,
};
let json = serde_json::to_string(&info).unwrap();
let back: ExportInfo = serde_json::from_str(&json).unwrap();
assert_eq!(info, back);
}
#[test]
fn verification_report_serde_roundtrip() {
let report = xll_utils::VerificationReport {
dll_path: std::path::PathBuf::from("test.xll"),
total_exports: 5,
found: vec!["a".into(), "b".into()],
missing: vec!["c".into()],
complete: false,
unexpected: vec!["d".into()],
mismatches: vec![xll_utils::NameMismatch {
expected: "Foo".into(),
actual: "foo".into(),
ordinal: 3,
}],
architecture: Architecture::X64,
};
let json = serde_json::to_string_pretty(&report).unwrap();
let back: xll_utils::VerificationReport = serde_json::from_str(&json).unwrap();
assert_eq!(back.dll_path, report.dll_path);
assert_eq!(back.total_exports, report.total_exports);
assert_eq!(back.complete, report.complete);
assert_eq!(back.found, report.found);
assert_eq!(back.missing, report.missing);
assert_eq!(back.mismatches, report.mismatches);
}
#[test]
fn xll_info_serde_roundtrip() {
let info = xll_info(fixture_path()).unwrap();
let json = serde_json::to_string_pretty(&info).unwrap();
let back: xll_utils::xll::XllInfo = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, info.name);
assert_eq!(back.architecture, info.architecture);
assert_eq!(back.export_count, info.export_count);
assert_eq!(back.has_auto_open, info.has_auto_open);
assert_eq!(back.exports, info.exports);
}
#[test]
fn export_diff_serde_roundtrip() {
let diff = xll_utils::ExportDiff {
missing: vec!["a".into()],
extra: vec!["b".into()],
common: vec!["c".into()],
};
let json = serde_json::to_string(&diff).unwrap();
let back: xll_utils::ExportDiff = serde_json::from_str(&json).unwrap();
assert_eq!(back.missing, diff.missing);
assert_eq!(back.extra, diff.extra);
assert_eq!(back.common, diff.common);
}
}