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 #[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 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 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 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}