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
55pub 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
72pub 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 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
100pub struct AbsolutePathNormalizer {
103 pattern: regex::bytes::Regex,
104}
105
106impl AbsolutePathNormalizer {
107 pub fn new() -> Self {
108 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
138pub 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 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 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}