Skip to main content

squeeze/
hash.rs

1use super::{Finder, RunClass, RunRule};
2use std::ops::Range;
3
4const MD5_LEN: usize = 32;
5const SHA1_LEN: usize = 40;
6const SHA256_LEN: usize = 64;
7const SHA512_LEN: usize = 128;
8
9const MD5_BIT: u8 = 1;
10const SHA1_BIT: u8 = 2;
11const SHA256_BIT: u8 = 4;
12const SHA512_BIT: u8 = 8;
13const ALL_BITS: u8 = MD5_BIT | SHA1_BIT | SHA256_BIT | SHA512_BIT;
14
15#[derive(Default)]
16pub struct Hash {
17    lengths: u8,
18}
19
20impl Hash {
21    /// Restricts matching to the given algorithm. Returns `false` (leaving the
22    /// finder unchanged) when the algorithm is unknown, so callers can surface
23    /// an error instead of silently matching everything.
24    #[must_use]
25    pub fn add_algorithm(&mut self, name: &str) -> bool {
26        let bit = match name.to_lowercase().as_str() {
27            "md5" => MD5_BIT,
28            "sha1" | "sha-1" => SHA1_BIT,
29            "sha256" | "sha-256" => SHA256_BIT,
30            "sha512" | "sha-512" => SHA512_BIT,
31            _ => return false,
32        };
33        self.lengths |= bit;
34        true
35    }
36
37    fn is_target_length(&self, len: usize) -> bool {
38        let bit = match len {
39            MD5_LEN => MD5_BIT,
40            SHA1_LEN => SHA1_BIT,
41            SHA256_LEN => SHA256_BIT,
42            SHA512_LEN => SHA512_BIT,
43            _ => return false,
44        };
45        let mask = if self.lengths == 0 {
46            ALL_BITS
47        } else {
48            self.lengths
49        };
50        (mask & bit) != 0
51    }
52
53    fn is_hex(b: u8) -> bool {
54        b.is_ascii_hexdigit()
55    }
56}
57
58impl Finder for Hash {
59    fn line_agnostic(&self) -> bool {
60        // Matches never contain a line terminator and `\n`/`\r` end every
61        // walk exactly like the end of the input does.
62        true
63    }
64
65    fn id(&self) -> &'static str {
66        "hash"
67    }
68
69    fn dispatchable(&self) -> bool {
70        true
71    }
72
73    fn could_start_at(&self, byte: u8) -> bool {
74        byte.is_ascii_hexdigit()
75    }
76
77    fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
78        !Self::is_hex(prev)
79    }
80
81    fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
82        Self::is_hex(next)
83    }
84
85    fn run_rules(&self) -> Vec<RunRule> {
86        let mask = if self.lengths == 0 {
87            ALL_BITS
88        } else {
89            self.lengths
90        };
91        let lengths = [
92            (MD5_BIT, MD5_LEN),
93            (SHA1_BIT, SHA1_LEN),
94            (SHA256_BIT, SHA256_LEN),
95            (SHA512_BIT, SHA512_LEN),
96        ];
97        let enabled = lengths.iter().filter(|(bit, _)| mask & bit != 0);
98        let min = enabled
99            .clone()
100            .map(|(_, len)| *len)
101            .min()
102            .unwrap_or(MD5_LEN);
103        let max = enabled.map(|(_, len)| *len).max().unwrap_or(SHA512_LEN);
104        vec![RunRule::new(RunClass::Hex, min as u8, max as u8)]
105    }
106
107    fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
108        if !Self::is_hex(input[pos]) {
109            return None;
110        }
111        if pos > 0 && Self::is_hex(input[pos - 1]) {
112            return None;
113        }
114        let mut end = pos;
115        while end < input.len() && Self::is_hex(input[end]) {
116            end += 1;
117        }
118        if self.is_target_length(end - pos) {
119            Some(pos..end)
120        } else {
121            None
122        }
123    }
124
125    fn find(&self, s: &str) -> Option<Range<usize>> {
126        let input = s.as_bytes();
127        let mut idx = 0;
128
129        while idx < input.len() {
130            if !Self::is_hex(input[idx]) {
131                idx += 1;
132                continue;
133            }
134
135            // Check boundary before
136            if idx > 0 && Self::is_hex(input[idx - 1]) {
137                idx += 1;
138                continue;
139            }
140
141            let start = idx;
142            let mut end = start;
143            while end < input.len() && Self::is_hex(input[end]) {
144                end += 1;
145            }
146
147            let len = end - start;
148
149            if self.is_target_length(len) {
150                return Some(start..end);
151            }
152
153            idx = end;
154        }
155
156        None
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn id_should_return_hash() {
166        let finder = Hash::default();
167        assert_eq!("hash", finder.id());
168    }
169
170    #[test]
171    fn find_should_extract_md5() {
172        let finder = Hash::default();
173        let input = "md5: 5d41402abc4b2a76b9719d911017c592";
174        let range = finder.find(input).unwrap();
175        assert_eq!("5d41402abc4b2a76b9719d911017c592", &input[range]);
176    }
177
178    #[test]
179    fn find_should_extract_sha1() {
180        let finder = Hash::default();
181        let input = "sha1: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed";
182        let range = finder.find(input).unwrap();
183        assert_eq!("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", &input[range]);
184    }
185
186    #[test]
187    fn find_should_extract_sha256() {
188        let finder = Hash::default();
189        let input = "sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
190        let range = finder.find(input).unwrap();
191        assert_eq!(
192            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
193            &input[range]
194        );
195    }
196
197    #[test]
198    fn find_should_extract_sha512() {
199        let finder = Hash::default();
200        let input = "sha512: cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e";
201        let range = finder.find(input).unwrap();
202        assert_eq!(128, input[range].len());
203    }
204
205    #[test]
206    fn find_should_extract_uppercase_hash() {
207        let finder = Hash::default();
208        let input = "5D41402ABC4B2A76B9719D911017C592";
209        let range = finder.find(input).unwrap();
210        assert_eq!("5D41402ABC4B2A76B9719D911017C592", &input[range]);
211    }
212
213    #[test]
214    fn find_should_filter_by_algorithm() {
215        let mut finder = Hash::default();
216        assert!(finder.add_algorithm("sha256"));
217
218        let md5 = "5d41402abc4b2a76b9719d911017c592";
219        assert!(finder.find(md5).is_none());
220
221        let sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
222        assert!(finder.find(sha256).is_some());
223    }
224
225    #[test]
226    fn find_should_filter_md5_only() {
227        let mut finder = Hash::default();
228        assert!(finder.add_algorithm("md5"));
229
230        let input = "5d41402abc4b2a76b9719d911017c592";
231        assert!(finder.find(input).is_some());
232
233        let sha1 = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed";
234        assert!(finder.find(sha1).is_none());
235    }
236
237    #[test]
238    fn find_should_not_match_short_hex() {
239        let finder = Hash::default();
240        assert!(finder.find("deadbeef").is_none());
241    }
242
243    #[test]
244    fn find_should_not_match_within_longer_hex() {
245        let finder = Hash::default();
246        // 33 hex chars — not a valid hash length, and should not partially match
247        let input = "a5d41402abc4b2a76b9719d911017c592";
248        assert!(finder.find(input).is_none());
249    }
250
251    #[test]
252    fn find_should_not_match_with_trailing_hex() {
253        let finder = Hash::default();
254        let input = "5d41402abc4b2a76b9719d911017c592a";
255        assert!(finder.find(input).is_none());
256    }
257
258    #[test]
259    fn find_should_extract_hash_in_text() {
260        let finder = Hash::default();
261        let input = "commit 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed in main";
262        let range = finder.find(input).unwrap();
263        assert_eq!("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", &input[range]);
264    }
265
266    #[test]
267    fn find_should_handle_empty_input() {
268        let finder = Hash::default();
269        assert!(finder.find("").is_none());
270    }
271
272    #[test]
273    fn find_should_extract_multiple_hashes_iteratively() {
274        let finder = Hash::default();
275        let input = "5d41402abc4b2a76b9719d911017c592 and 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed";
276
277        let mut results = Vec::new();
278        let mut idx = 0;
279        while idx < input.len() {
280            if let Some(range) = finder.find(&input[idx..]) {
281                results.push(&input[idx + range.start..idx + range.end]);
282                idx += range.end;
283            } else {
284                break;
285            }
286        }
287
288        assert_eq!(
289            vec![
290                "5d41402abc4b2a76b9719d911017c592",
291                "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
292            ],
293            results
294        );
295    }
296
297    #[test]
298    fn add_algorithm_should_accept_various_names() {
299        let mut finder = Hash::default();
300        assert!(finder.add_algorithm("sha-256"));
301        assert!(finder.lengths & SHA256_BIT != 0);
302
303        assert!(finder.add_algorithm("SHA1"));
304        assert!(finder.lengths & SHA1_BIT != 0);
305
306        assert!(finder.add_algorithm("SHA-512"));
307        assert!(finder.lengths & SHA512_BIT != 0);
308    }
309
310    #[test]
311    fn add_algorithm_should_reject_unknown() {
312        let mut finder = Hash::default();
313        assert!(!finder.add_algorithm("blake2"));
314        assert_eq!(0, finder.lengths);
315    }
316
317    #[test]
318    fn try_at_matches_find_at_start() {
319        let finder = Hash::default();
320        let input = b"5d41402abc4b2a76b9719d911017c592 trail";
321        assert!(finder.try_at(input, 0).is_some());
322        assert_eq!(finder.try_at(input, 0).unwrap(), 0..32);
323    }
324
325    #[test]
326    fn try_at_rejects_mid_hex_run() {
327        let finder = Hash::default();
328        let input = b"a5d41402abc4b2a76b9719d911017c592";
329        assert!(finder.try_at(input, 1).is_none());
330    }
331
332    #[test]
333    fn try_at_at_last_byte() {
334        let finder = Hash::default();
335        let input = b"x";
336        assert!(finder.try_at(input, 0).is_none());
337    }
338
339    #[test]
340    fn try_at_single_hex_byte() {
341        let finder = Hash::default();
342        let input = b"a";
343        assert!(finder.try_at(input, 0).is_none());
344    }
345
346    #[test]
347    fn find_should_not_match_non_hex_chars() {
348        let finder = Hash::default();
349        assert!(finder.find("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").is_none());
350    }
351
352    #[test]
353    fn find_should_match_all_hex_letters() {
354        let finder = Hash::default();
355        let input = "abcdefABCDEFabcdefABCDEFabcdefAB";
356        assert_eq!(finder.find(input).unwrap(), 0..32);
357    }
358}