Skip to main content

binate_core/
diff.rs

1use crate::binary::{BinaryProvider, MmapBinaryProvider};
2use crate::disasm;
3use crate::dwarf::{DwarfIndex, SourceLocation};
4use crate::error::Result;
5use crate::normalize::NormalizerChain;
6use crate::symbol::ResolvedSymbol;
7use rayon::prelude::*;
8use serde::Serialize;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12const MERGE_THRESHOLD: usize = 8;
13
14#[derive(Debug, Clone, Serialize)]
15#[non_exhaustive]
16pub struct ByteRange {
17    pub offset: u64,
18    pub len: usize,
19    pub left_bytes: Vec<u8>,
20    pub right_bytes: Vec<u8>,
21}
22
23#[derive(Debug, Clone, Serialize)]
24#[non_exhaustive]
25pub struct SymbolDiff {
26    pub symbol: ResolvedSymbol,
27    pub source_location: Option<SourceLocation>,
28    pub ranges: Vec<ByteRange>,
29    pub disasm_left: Option<Vec<String>>,
30    pub disasm_right: Option<Vec<String>>,
31}
32
33#[derive(Debug, Clone, Serialize)]
34#[non_exhaustive]
35pub struct AnonymousDiff {
36    pub section: String,
37    pub ranges: Vec<ByteRange>,
38}
39
40#[derive(Debug, Clone, Serialize)]
41#[non_exhaustive]
42pub struct DiffResult {
43    pub left_path: PathBuf,
44    pub right_path: PathBuf,
45    pub identical: bool,
46    pub symbol_diffs: Vec<SymbolDiff>,
47    pub anonymous_diffs: Vec<AnonymousDiff>,
48    pub sections_only_in_left: Vec<String>,
49    pub sections_only_in_right: Vec<String>,
50}
51
52pub struct DiffConfig {
53    pub normalizer: NormalizerChain,
54    pub show_disasm: bool,
55    pub parallel: bool,
56    pub ignored_sections: Vec<String>,
57}
58
59impl Default for DiffConfig {
60    fn default() -> Self {
61        Self {
62            normalizer: NormalizerChain::default(),
63            show_disasm: false,
64            parallel: true,
65            ignored_sections: Vec::new(),
66        }
67    }
68}
69
70// Compile-time check: DiffResult must be Send + Sync so it can cross thread boundaries.
71const _: fn() = || {
72    fn assert_send_sync<T: Send + Sync>() {}
73    assert_send_sync::<DiffResult>();
74};
75
76pub struct SemanticDiff;
77
78impl SemanticDiff {
79    /// Compare two binaries by file path using default normalization.
80    /// Convenience wrapper around [`compare`] for the common case.
81    pub fn compare_paths(left: &Path, right: &Path) -> Result<DiffResult> {
82        let left_bin = MmapBinaryProvider::open(left)?;
83        let right_bin = MmapBinaryProvider::open(right)?;
84        Self::compare(&left_bin, &right_bin, &DiffConfig::default())
85    }
86
87    pub fn compare(
88        left: &dyn BinaryProvider,
89        right: &dyn BinaryProvider,
90        config: &DiffConfig,
91    ) -> Result<DiffResult> {
92        // 1. Enumerate sections
93        let mut left_sections: HashMap<String, (Vec<u8>, u64)> = HashMap::new();
94        let mut right_sections: HashMap<String, (Vec<u8>, u64)> = HashMap::new();
95
96        left.visit_sections(&mut |name, _kind, addr, data| {
97            if !config.ignored_sections.iter().any(|s| s == name) {
98                left_sections.insert(name.to_owned(), (data.to_vec(), addr));
99            }
100            Ok(())
101        })?;
102
103        right.visit_sections(&mut |name, _kind, addr, data| {
104            if !config.ignored_sections.iter().any(|s| s == name) {
105                right_sections.insert(name.to_owned(), (data.to_vec(), addr));
106            }
107            Ok(())
108        })?;
109
110        let sections_only_in_left: Vec<String> = left_sections
111            .keys()
112            .filter(|k| !right_sections.contains_key(*k))
113            .cloned()
114            .collect();
115
116        let sections_only_in_right: Vec<String> = right_sections
117            .keys()
118            .filter(|k| !left_sections.contains_key(*k))
119            .cloned()
120            .collect();
121
122        let shared: Vec<String> = left_sections
123            .keys()
124            .filter(|k| right_sections.contains_key(*k))
125            .cloned()
126            .collect();
127
128        // 2 & 3. Normalize + byte diff + symbol attribution (per section, parallel)
129        let left_syms = left.symbol_table();
130
131        let section_results: Vec<Result<(Vec<SymbolDiff>, Vec<AnonymousDiff>)>> = if config.parallel {
132            shared
133                .par_iter()
134                .map(|name| {
135                    diff_section(
136                        name,
137                        &left_sections[name],
138                        &right_sections[name],
139                        &config.normalizer,
140                        left_syms,
141                    )
142                })
143                .collect()
144        } else {
145            shared
146                .iter()
147                .map(|name| {
148                    diff_section(
149                        name,
150                        &left_sections[name],
151                        &right_sections[name],
152                        &config.normalizer,
153                        left_syms,
154                    )
155                })
156                .collect()
157        };
158
159        let mut symbol_diffs: Vec<SymbolDiff> = Vec::new();
160        let mut anonymous_diffs: Vec<AnonymousDiff> = Vec::new();
161
162        for result in section_results {
163            let (syms, anons) = result?;
164            symbol_diffs.extend(syms);
165            anonymous_diffs.extend(anons);
166        }
167
168        // 4. DWARF enrichment (lazy)
169        if !symbol_diffs.is_empty() {
170            if let Ok(dwarf) = DwarfIndex::load(left) {
171                for sd in &mut symbol_diffs {
172                    sd.source_location = dwarf.resolve(sd.symbol.address).ok().flatten();
173                }
174            }
175        }
176
177        // 5. Optional disassembly
178        if config.show_disasm {
179            if let Some(bitness) = disasm::bitness_for(left.architecture()) {
180                for sd in &mut symbol_diffs {
181                    let addr = sd.symbol.address;
182                    let size = sd.symbol.size as usize;
183
184                    let left_bytes = left
185                        .section_data(sd.symbol.section.as_deref().unwrap_or(".text"))
186                        .and_then(|(data, sec_addr, _)| {
187                            let off = addr.saturating_sub(sec_addr) as usize;
188                            data.get(off..off + size)
189                        });
190
191                    let right_bytes = right
192                        .section_data(sd.symbol.section.as_deref().unwrap_or(".text"))
193                        .and_then(|(data, sec_addr, _)| {
194                            let off = addr.saturating_sub(sec_addr) as usize;
195                            data.get(off..off + size)
196                        });
197
198                    if let Some(lb) = left_bytes {
199                        sd.disasm_left = Some(disasm::disassemble(lb, addr, bitness));
200                    }
201                    if let Some(rb) = right_bytes {
202                        sd.disasm_right = Some(disasm::disassemble(rb, addr, bitness));
203                    }
204                }
205            }
206        }
207
208        let identical = symbol_diffs.is_empty() && anonymous_diffs.is_empty();
209
210        Ok(DiffResult {
211            left_path: left.path().to_path_buf(),
212            right_path: right.path().to_path_buf(),
213            identical,
214            symbol_diffs,
215            anonymous_diffs,
216            sections_only_in_left,
217            sections_only_in_right,
218        })
219    }
220}
221
222fn diff_section(
223    name: &str,
224    left: &(Vec<u8>, u64),
225    right: &(Vec<u8>, u64),
226    normalizer: &NormalizerChain,
227    syms: &crate::symbol::SymbolTable,
228) -> Result<(Vec<SymbolDiff>, Vec<AnonymousDiff>)> {
229    let (left_data, left_addr) = left;
230    let (right_data, _right_addr) = right;
231
232    let left_norm = normalizer.apply(name, left_data);
233    let right_norm = normalizer.apply(name, right_data);
234
235    if left_norm == right_norm {
236        return Ok((Vec::new(), Vec::new()));
237    }
238
239    let ranges = find_differing_ranges(&left_norm, &right_norm, *left_addr);
240
241    // Group ranges by symbol
242    let mut by_symbol: HashMap<u64, (ResolvedSymbol, Vec<ByteRange>)> = HashMap::new();
243    let mut anon_ranges: Vec<ByteRange> = Vec::new();
244
245    for range in ranges {
246        let addr = range.offset;
247        if let Some(sym) = syms.symbol_at(addr) {
248            let entry = by_symbol
249                .entry(sym.address)
250                .or_insert_with(|| (sym.clone(), Vec::new()));
251            entry.1.push(range);
252        } else {
253            anon_ranges.push(range);
254        }
255    }
256
257    let sym_diffs: Vec<SymbolDiff> = by_symbol
258        .into_values()
259        .map(|(symbol, ranges)| SymbolDiff {
260            symbol,
261            source_location: None,
262            ranges,
263            disasm_left: None,
264            disasm_right: None,
265        })
266        .collect();
267
268    let anon_diffs = if anon_ranges.is_empty() {
269        Vec::new()
270    } else {
271        vec![AnonymousDiff { section: name.to_owned(), ranges: anon_ranges }]
272    };
273
274    Ok((sym_diffs, anon_diffs))
275}
276
277/// O(n) linear scan producing `ByteRange`s at virtual addresses.
278/// Runs of identical bytes >= MERGE_THRESHOLD end the current differing range.
279fn find_differing_ranges(left: &[u8], right: &[u8], base_addr: u64) -> Vec<ByteRange> {
280    let len = left.len().min(right.len());
281    let mut ranges: Vec<ByteRange> = Vec::new();
282    let mut i = 0;
283
284    while i < len {
285        if left[i] == right[i] {
286            i += 1;
287            continue;
288        }
289        // Start of a differing run
290        let start = i;
291        i += 1;
292        while i < len {
293            if left[i] == right[i] {
294                // Check if we have MERGE_THRESHOLD identical bytes ahead
295                let run_end = (i + MERGE_THRESHOLD).min(len);
296                if left[i..run_end] == right[i..run_end] {
297                    break;
298                }
299            }
300            i += 1;
301        }
302        let end = i;
303        ranges.push(ByteRange {
304            offset: base_addr + start as u64,
305            len: end - start,
306            left_bytes: left[start..end].to_vec(),
307            right_bytes: right[start..end].to_vec(),
308        });
309    }
310
311    // Handle trailing extra bytes if sizes differ
312    if left.len() != right.len() {
313        let extra_start = len;
314        let left_extra = &left[extra_start..];
315        let right_extra = &right[extra_start..];
316        if !left_extra.is_empty() || !right_extra.is_empty() {
317            ranges.push(ByteRange {
318                offset: base_addr + extra_start as u64,
319                len: left_extra.len().max(right_extra.len()),
320                left_bytes: left_extra.to_vec(),
321                right_bytes: right_extra.to_vec(),
322            });
323        }
324    }
325
326    ranges
327}