Skip to main content

xll_utils/
exports.rs

1//! Export enumeration and verification utilities.
2//!
3//! Provides functions for listing DLL exports, verifying that expected
4//! exports are present, and computing diffs between export lists.
5
6use std::collections::HashSet;
7use std::path::Path;
8
9use crate::error::Result;
10use crate::pe::parse_pe_file;
11use crate::types::{ExportDiff, ExportInfo, NameMismatch, PeFile, VerificationReport};
12
13/// List all exports from a DLL/XLL file.
14///
15/// # Examples
16///
17/// ```no_run
18/// use xll_utils::exports::list_dll_exports;
19///
20/// let exports = list_dll_exports("my_xll.xll").unwrap();
21/// for exp in &exports {
22///     if let Some(name) = &exp.name {
23///         println!("{name} (ordinal {})", exp.ordinal);
24///     }
25/// }
26/// ```
27pub fn list_dll_exports(path: impl AsRef<Path>) -> Result<Vec<ExportInfo>> {
28    let pe = parse_pe_file(path.as_ref())?;
29    Ok(pe.exports().to_vec())
30}
31
32/// List export names from a DLL/XLL file (sorted).
33pub fn list_export_names(path: impl AsRef<Path>) -> Result<Vec<String>> {
34    let pe = parse_pe_file(path.as_ref())?;
35    Ok(pe.export_names())
36}
37
38/// Verify that a DLL/XLL contains all expected exports.
39///
40/// # Examples
41///
42/// ```no_run
43/// use xll_utils::exports::verify_dll_exports;
44///
45/// let report = verify_dll_exports("my_xll.xll", &["xlAutoOpen", "xlAutoFree12"]).unwrap();
46/// assert!(report.complete, "Missing exports: {:?}", report.missing);
47/// ```
48pub fn verify_dll_exports(
49    path: impl AsRef<Path>,
50    expected: &[&str],
51) -> Result<VerificationReport> {
52    let pe = parse_pe_file(path.as_ref())?;
53    Ok(verify_pe_exports(&pe, expected))
54}
55
56/// Verify exports from an already-parsed PE file.
57///
58/// This is a pure computation on an already-parsed `PeFile` and always succeeds.
59///
60/// # Examples
61///
62/// ```no_run
63/// use xll_utils::pe::parse_pe_file;
64/// use xll_utils::exports::verify_pe_exports;
65///
66/// let pe = parse_pe_file("my_xll.xll").unwrap();
67/// let report = verify_pe_exports(&pe, &["xlAutoOpen"]);
68/// println!("complete: {}", report.complete);
69/// ```
70pub fn verify_pe_exports(pe: &PeFile, expected: &[&str]) -> VerificationReport {
71    let export_names: HashSet<&str> = pe
72        .exports
73        .iter()
74        .filter_map(|e| e.name.as_deref())
75        .collect();
76
77    let expected_set: HashSet<&str> = expected.iter().copied().collect();
78
79    let mut found = Vec::new();
80    let mut missing = Vec::new();
81
82    for &name in expected {
83        if export_names.contains(name) {
84            found.push(name.to_string());
85        } else {
86            missing.push(name.to_string());
87        }
88    }
89
90    let mut unexpected: Vec<String> = export_names
91        .iter()
92        .filter(|n| !expected_set.contains(*n))
93        .map(|s| s.to_string())
94        .collect();
95    unexpected.sort();
96
97    let complete = missing.is_empty();
98
99    VerificationReport {
100        dll_path: pe.path.clone(),
101        total_exports: pe.exports.len(),
102        found,
103        missing,
104        complete,
105        unexpected,
106        mismatches: Vec::new(),
107        architecture: pe.architecture,
108    }
109}
110
111/// Verify exports with case-insensitive name matching.
112///
113/// When `case_sensitive` is `false`, exports are matched ignoring case and
114/// any case mismatches are reported in `VerificationReport::mismatches`.
115pub fn verify_dll_exports_strict(
116    path: impl AsRef<Path>,
117    expected: &[&str],
118    case_sensitive: bool,
119) -> Result<VerificationReport> {
120    let pe = parse_pe_file(path.as_ref())?;
121    let mut report = verify_pe_exports(&pe, expected);
122
123    if !case_sensitive {
124        // Re-evaluate found/missing using case-insensitive matching.
125        // Use a Vec of (lowercase_name, &ExportInfo) to avoid HashMap
126        // collisions when multiple exports differ only by case.
127        let exports_lower: Vec<(String, &ExportInfo)> = pe
128            .exports
129            .iter()
130            .filter_map(|e| e.name.as_deref().map(|n| (n.to_lowercase(), e)))
131            .collect();
132
133        let mut found = Vec::new();
134        let mut missing = Vec::new();
135        let mut mismatches = Vec::new();
136
137        for &name in expected {
138            let key = name.to_lowercase();
139            // Find the first matching export (case-insensitive).
140            if let Some((_, export)) = exports_lower.iter().find(|(lc, _)| *lc == key) {
141                found.push(name.to_string());
142                // Check for case mismatch.
143                let actual_name = export
144                    .name
145                    .as_deref()
146                    .expect("export has name (filtered above)");
147                if actual_name != name {
148                    mismatches.push(NameMismatch {
149                        expected: name.to_string(),
150                        actual: actual_name.to_string(),
151                        ordinal: export.ordinal,
152                    });
153                }
154            } else {
155                missing.push(name.to_string());
156            }
157        }
158
159        let expected_lower_set: HashSet<String> =
160            expected.iter().map(|s| s.to_lowercase()).collect();
161        let mut unexpected: Vec<String> = pe
162            .exports
163            .iter()
164            .filter_map(|e| e.name.as_deref())
165            .filter(|n| !expected_lower_set.contains(&n.to_lowercase()))
166            .map(|s| s.to_string())
167            .collect();
168        unexpected.sort();
169
170        report.found = found;
171        report.missing = missing;
172        report.complete = report.missing.is_empty();
173        report.unexpected = unexpected;
174        report.mismatches = mismatches;
175    }
176
177    Ok(report)
178}
179
180/// Compute differences between expected and actual export lists.
181///
182/// # Examples
183///
184/// ```
185/// use xll_utils::exports::export_diff;
186/// use xll_utils::ExportInfo;
187///
188/// let actual = vec![
189///     ExportInfo { name: Some("foo".into()), ordinal: 1, is_forwarded: false, forward_to: None, relative_address: Some(0x1000) },
190///     ExportInfo { name: Some("bar".into()), ordinal: 2, is_forwarded: false, forward_to: None, relative_address: Some(0x2000) },
191/// ];
192/// let diff = export_diff(&["foo", "baz"], &actual);
193/// assert_eq!(diff.common, vec!["foo"]);
194/// assert_eq!(diff.missing, vec!["baz"]);
195/// assert_eq!(diff.extra, vec!["bar"]);
196/// ```
197pub fn export_diff(expected: &[&str], actual: &[ExportInfo]) -> ExportDiff {
198    let actual_names: HashSet<&str> = actual
199        .iter()
200        .filter_map(|e| e.name.as_deref())
201        .collect();
202
203    let expected_set: HashSet<&str> = expected.iter().copied().collect();
204
205    let missing = expected
206        .iter()
207        .filter(|e| !actual_names.contains(*e))
208        .map(|s| s.to_string())
209        .collect();
210
211    let mut extra: Vec<String> = actual_names
212        .iter()
213        .filter(|e| !expected_set.contains(*e))
214        .map(|s| s.to_string())
215        .collect();
216    extra.sort();
217
218    let common = expected
219        .iter()
220        .filter(|e| actual_names.contains(*e))
221        .map(|s| s.to_string())
222        .collect();
223
224    ExportDiff {
225        missing,
226        extra,
227        common,
228    }
229}