Skip to main content

binate_core/
normalize.rs

1use std::borrow::Cow;
2
3pub trait Normalizer: Send + Sync {
4    fn name(&self) -> &'static str;
5    fn normalize<'a>(&self, section: &str, data: &'a [u8]) -> Cow<'a, [u8]>;
6}
7
8pub struct NormalizerChain {
9    inner: Vec<Box<dyn Normalizer>>,
10}
11
12impl NormalizerChain {
13    pub fn new() -> Self {
14        Self { inner: Vec::new() }
15    }
16
17    pub fn push(mut self, n: impl Normalizer + 'static) -> Self {
18        self.inner.push(Box::new(n));
19        self
20    }
21
22    pub fn push_boxed(mut self, n: Box<dyn Normalizer>) -> Self {
23        self.inner.push(n);
24        self
25    }
26
27    pub fn apply<'a>(&self, section: &str, data: &'a [u8]) -> Cow<'a, [u8]> {
28        let mut result: Cow<'a, [u8]> = Cow::Borrowed(data);
29        for norm in &self.inner {
30            match norm.normalize(section, &result) {
31                Cow::Borrowed(_) => {}
32                Cow::Owned(owned) => {
33                    result = Cow::Owned(owned);
34                }
35            }
36        }
37        result
38    }
39
40    pub fn names(&self) -> Vec<&'static str> {
41        self.inner.iter().map(|n| n.name()).collect()
42    }
43}
44
45impl Default for NormalizerChain {
46    fn default() -> Self {
47        Self::new()
48            .push(BuildIdNormalizer)
49            .push(TimestampNormalizer)
50            .push(AbsolutePathNormalizer::new())
51            .push(LinkerVersionNormalizer)
52    }
53}
54
55// ── BuildIdNormalizer ────────────────────────────────────────────────────────
56
57pub struct BuildIdNormalizer;
58
59impl Normalizer for BuildIdNormalizer {
60    fn name(&self) -> &'static str {
61        "build-id"
62    }
63
64    fn normalize<'a>(&self, section: &str, data: &'a [u8]) -> Cow<'a, [u8]> {
65        if !section.starts_with(".note") && section != "__DATA,__uuid" {
66            return Cow::Borrowed(data);
67        }
68        Cow::Owned(vec![0u8; data.len()])
69    }
70}
71
72// ── TimestampNormalizer ──────────────────────────────────────────────────────
73
74pub struct TimestampNormalizer;
75
76impl Normalizer for TimestampNormalizer {
77    fn name(&self) -> &'static str {
78        "timestamp"
79    }
80
81    fn normalize<'a>(&self, section: &str, data: &'a [u8]) -> Cow<'a, [u8]> {
82        if section != ".comment" {
83            return Cow::Borrowed(data);
84        }
85        // Zero out any 4-byte little-endian values that look like Unix timestamps
86        // (between 0x3B9ACA00 / 2001-01-01 and 0x7FFFFFFF / 2038-01-19)
87        const TS_MIN: u32 = 0x3B9A_CA00;
88        const TS_MAX: u32 = 0x7FFF_FFFF;
89        let mut out = data.to_vec();
90        for i in 0..out.len().saturating_sub(3) {
91            let v = u32::from_le_bytes([out[i], out[i + 1], out[i + 2], out[i + 3]]);
92            if v >= TS_MIN && v <= TS_MAX {
93                out[i..i + 4].fill(0);
94            }
95        }
96        Cow::Owned(out)
97    }
98}
99
100// ── AbsolutePathNormalizer ───────────────────────────────────────────────────
101
102pub struct AbsolutePathNormalizer {
103    pattern: regex::bytes::Regex,
104}
105
106impl AbsolutePathNormalizer {
107    pub fn new() -> Self {
108        // Matches Unix absolute paths like /Users/... or /home/...
109        let pattern = regex::bytes::Regex::new(r"/[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)+")
110            .expect("valid regex");
111        Self { pattern }
112    }
113}
114
115impl Normalizer for AbsolutePathNormalizer {
116    fn name(&self) -> &'static str {
117        "absolute-path"
118    }
119
120    fn normalize<'a>(&self, section: &str, data: &'a [u8]) -> Cow<'a, [u8]> {
121        if !matches!(section, ".debug_str" | ".debug_line_str" | ".comment" | "__DWARF,__debug_str") {
122            return Cow::Borrowed(data);
123        }
124        let replaced = self.pattern.replace_all(data, b"<PATH>".as_slice());
125        match replaced {
126            Cow::Borrowed(_) => Cow::Borrowed(data),
127            Cow::Owned(v) => Cow::Owned(v),
128        }
129    }
130}
131
132impl Default for AbsolutePathNormalizer {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138// ── LinkerVersionNormalizer ──────────────────────────────────────────────────
139
140pub struct LinkerVersionNormalizer;
141
142impl Normalizer for LinkerVersionNormalizer {
143    fn name(&self) -> &'static str {
144        "linker-version"
145    }
146
147    fn normalize<'a>(&self, section: &str, data: &'a [u8]) -> Cow<'a, [u8]> {
148        if section != ".comment" {
149            return Cow::Borrowed(data);
150        }
151        // Zero out null-terminated strings that look like linker version banners
152        // e.g. "Linker: LLD 18.0.0", "GCC: (GNU) 14.2.0"
153        let mut out = data.to_vec();
154        let keywords: &[&[u8]] = &[b"Linker:", b"GCC:", b"clang version", b"LLVM"];
155        'outer: for i in 0..out.len() {
156            for kw in keywords {
157                if out[i..].starts_with(kw) {
158                    // Zero from i to the next null byte (end of C string)
159                    let end = out[i..].iter().position(|&b| b == 0).map(|p| i + p + 1).unwrap_or(out.len());
160                    out[i..end].fill(0);
161                    continue 'outer;
162                }
163            }
164        }
165        Cow::Owned(out)
166    }
167}