1use super::{Anchor, ByteSet, Finder, RunClass, RunRule};
2use std::ops::Range;
3
4#[derive(Default)]
5pub struct Uuid {}
6
7impl Uuid {
8 fn is_hex(b: u8) -> bool {
9 b.is_ascii_hexdigit()
10 }
11
12 fn check_pattern(input: &[u8], start: usize) -> bool {
14 let Some(bytes) = input.get(start..start + 36) else {
15 return false;
16 };
17 let bytes: &[u8; 36] = bytes.try_into().expect("36 bytes");
18 #[cfg(target_arch = "aarch64")]
19 {
20 Self::check_pattern_neon(bytes)
21 }
22 #[cfg(not(target_arch = "aarch64"))]
23 {
24 Self::check_pattern_scalar(bytes)
25 }
26 }
27
28 #[cfg(any(test, not(target_arch = "aarch64")))]
29 fn check_pattern_scalar(bytes: &[u8; 36]) -> bool {
30 bytes.iter().enumerate().all(|(i, &b)| match i {
31 8 | 13 | 18 | 23 => b == b'-',
32 _ => Self::is_hex(b),
33 })
34 }
35
36 #[cfg(target_arch = "aarch64")]
39 fn check_pattern_neon(bytes: &[u8; 36]) -> bool {
40 use core::arch::aarch64::*;
41 const DASHES_LO: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0, 0, 0, 0, 0xFF, 0, 0];
43 const DASHES_HI: [u8; 16] = [0, 0, 0xFF, 0, 0, 0, 0, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
44 unsafe {
47 let hex_ok = |v: uint8x16_t| -> uint8x16_t {
48 let digit = vcltq_u8(vsubq_u8(v, vdupq_n_u8(b'0')), vdupq_n_u8(10));
49 let lower = vorrq_u8(v, vdupq_n_u8(0x20));
50 let alpha = vcltq_u8(vsubq_u8(lower, vdupq_n_u8(b'a')), vdupq_n_u8(6));
51 vorrq_u8(digit, alpha)
52 };
53 let check = |v: uint8x16_t, dashes: uint8x16_t| -> bool {
54 let is_dash = vceqq_u8(v, vdupq_n_u8(b'-'));
55 let ok = vbslq_u8(dashes, is_dash, hex_ok(v));
57 vminvq_u8(ok) == 0xFF
58 };
59 let lo = vld1q_u8(bytes.as_ptr());
60 let hi = vld1q_u8(bytes.as_ptr().add(16));
61 check(lo, vld1q_u8(DASHES_LO.as_ptr()))
62 && check(hi, vld1q_u8(DASHES_HI.as_ptr()))
63 && bytes[32..36].iter().all(|&b| Self::is_hex(b))
64 }
65 }
66}
67
68impl Finder for Uuid {
69 fn line_agnostic(&self) -> bool {
70 true
73 }
74
75 fn id(&self) -> &'static str {
76 "uuid"
77 }
78
79 fn dispatchable(&self) -> bool {
80 true
81 }
82
83 fn could_start_at(&self, byte: u8) -> bool {
84 byte.is_ascii_hexdigit()
85 }
86
87 fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
88 !(Self::is_hex(prev) || prev == b'-')
89 }
90
91 fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
92 Self::is_hex(next)
93 }
94
95 fn anchor(&self) -> Option<Anchor> {
96 Some(
98 Anchor::new(
99 ByteSet::from_bytes(b"-"),
100 ByteSet::from_fn(|b| b.is_ascii_hexdigit()),
101 )
102 .confirm_all(b"-", &[5, 10, 15], b"-")
103 .back(8),
104 )
105 }
106
107 fn run_rules(&self) -> Vec<RunRule> {
108 vec![
110 RunRule::new(RunClass::Hex, 8, 8)
111 .followed_by(b"-")
112 .then(RunClass::Hex, 4, 4)
113 .followed_by(b"-")
114 .then(RunClass::Hex, 4, 4)
115 .followed_by(b"-"),
116 ]
117 }
118
119 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
120 if !Self::is_hex(input[pos]) {
121 return None;
122 }
123 if pos > 0 && (Self::is_hex(input[pos - 1]) || input[pos - 1] == b'-') {
124 return None;
125 }
126 if !Self::check_pattern(input, pos) {
127 return None;
128 }
129 let end = pos + 36;
130 if end < input.len() && (Self::is_hex(input[end]) || input[end] == b'-') {
131 return None;
132 }
133 Some(pos..end)
134 }
135
136 fn find(&self, s: &str) -> Option<Range<usize>> {
137 let input = s.as_bytes();
138 let mut idx = 0;
139
140 while idx + 36 <= input.len() {
141 if Self::is_hex(input[idx]) {
142 if idx > 0 && (Self::is_hex(input[idx - 1]) || input[idx - 1] == b'-') {
143 idx += 1;
144 continue;
145 }
146
147 if Self::check_pattern(input, idx) {
148 let end = idx + 36;
149 if end < input.len() && (Self::is_hex(input[end]) || input[end] == b'-') {
150 idx += 1;
151 continue;
152 }
153 return Some(idx..end);
154 }
155 }
156 idx += 1;
157 }
158
159 None
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn id_should_return_uuid() {
169 let finder = Uuid::default();
170 assert_eq!("uuid", finder.id());
171 }
172
173 #[test]
174 fn find_should_extract_uuid() {
175 let finder = Uuid::default();
176 let input = "id: 550e8400-e29b-41d4-a716-446655440000";
177 let range = finder.find(input).unwrap();
178 assert_eq!("550e8400-e29b-41d4-a716-446655440000", &input[range]);
179 }
180
181 #[test]
182 fn find_should_extract_uuid_at_start() {
183 let finder = Uuid::default();
184 let input = "550e8400-e29b-41d4-a716-446655440000 is the id";
185 let range = finder.find(input).unwrap();
186 assert_eq!("550e8400-e29b-41d4-a716-446655440000", &input[range]);
187 }
188
189 #[test]
190 fn find_should_extract_uppercase_uuid() {
191 let finder = Uuid::default();
192 let input = "550E8400-E29B-41D4-A716-446655440000";
193 let range = finder.find(input).unwrap();
194 assert_eq!("550E8400-E29B-41D4-A716-446655440000", &input[range]);
195 }
196
197 #[test]
198 fn find_should_extract_mixed_case_uuid() {
199 let finder = Uuid::default();
200 let input = "550e8400-E29B-41d4-A716-446655440000";
201 let range = finder.find(input).unwrap();
202 assert_eq!("550e8400-E29B-41d4-A716-446655440000", &input[range]);
203 }
204
205 #[test]
206 fn find_should_reject_no_dashes() {
207 let finder = Uuid::default();
208 assert!(finder.find("550e8400e29b41d4a716446655440000").is_none());
209 }
210
211 #[test]
212 fn find_should_reject_wrong_dash_positions() {
213 let finder = Uuid::default();
214 assert!(
215 finder
216 .find("550e840-0e29b-41d4-a716-446655440000")
217 .is_none()
218 );
219 }
220
221 #[test]
222 fn find_should_reject_too_short() {
223 let finder = Uuid::default();
224 assert!(finder.find("550e8400-e29b-41d4-a716").is_none());
225 }
226
227 #[test]
228 fn find_should_reject_non_hex() {
229 let finder = Uuid::default();
230 assert!(
231 finder
232 .find("550e8400-e29b-41d4-a716-44665544000g")
233 .is_none()
234 );
235 }
236
237 #[test]
238 fn find_should_not_match_within_longer_hex() {
239 let finder = Uuid::default();
240 assert!(
241 finder
242 .find("ff550e8400-e29b-41d4-a716-446655440000")
243 .is_none()
244 );
245 }
246
247 #[test]
248 fn find_should_not_match_with_trailing_hex() {
249 let finder = Uuid::default();
250 assert!(
251 finder
252 .find("550e8400-e29b-41d4-a716-446655440000ff")
253 .is_none()
254 );
255 }
256
257 #[test]
258 fn find_should_extract_uuid_in_brackets() {
259 let finder = Uuid::default();
260 let input = "[550e8400-e29b-41d4-a716-446655440000]";
261 let range = finder.find(input).unwrap();
262 assert_eq!("550e8400-e29b-41d4-a716-446655440000", &input[range]);
263 }
264
265 #[test]
266 fn find_should_handle_empty_input() {
267 let finder = Uuid::default();
268 assert!(finder.find("").is_none());
269 }
270
271 #[test]
272 fn find_should_extract_multiple_uuids_iteratively() {
273 let finder = Uuid::default();
274 let input = "550e8400-e29b-41d4-a716-446655440000 and 6ba7b810-9dad-11d1-80b4-00c04fd430c8";
275
276 let mut results = Vec::new();
277 let mut idx = 0;
278 while idx < input.len() {
279 if let Some(range) = finder.find(&input[idx..]) {
280 results.push(&input[idx + range.start..idx + range.end]);
281 idx += range.end;
282 } else {
283 break;
284 }
285 }
286
287 assert_eq!(
288 vec![
289 "550e8400-e29b-41d4-a716-446655440000",
290 "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
291 ],
292 results
293 );
294 }
295
296 #[test]
297 fn find_should_extract_nil_uuid() {
298 let finder = Uuid::default();
299 let input = "00000000-0000-0000-0000-000000000000";
300 let range = finder.find(input).unwrap();
301 assert_eq!("00000000-0000-0000-0000-000000000000", &input[range]);
302 }
303
304 #[test]
305 fn try_at_at_start() {
306 let finder = Uuid::default();
307 let input = b"550e8400-e29b-41d4-a716-446655440000 end";
308 assert_eq!(finder.try_at(input, 0), Some(0..36));
309 }
310
311 #[test]
312 fn try_at_preceded_by_hex() {
313 let finder = Uuid::default();
314 let input = b"ff550e8400-e29b-41d4-a716-446655440000";
315 assert!(finder.try_at(input, 2).is_none());
316 }
317
318 #[test]
319 fn try_at_preceded_by_dash() {
320 let finder = Uuid::default();
321 let input = b"-550e8400-e29b-41d4-a716-446655440000";
322 assert!(finder.try_at(input, 1).is_none());
323 }
324
325 #[test]
326 fn try_at_too_short_input() {
327 let finder = Uuid::default();
328 let input = b"550e8400";
329 assert!(finder.try_at(input, 0).is_none());
330 }
331
332 #[test]
333 fn find_should_reject_extra_dash_at_end() {
334 let finder = Uuid::default();
335 assert!(
336 finder
337 .find("550e8400-e29b-41d4-a716-446655440000-")
338 .is_none()
339 );
340 }
341
342 #[test]
343 fn find_should_handle_uuid_at_exact_end() {
344 let finder = Uuid::default();
345 let input = "x 550e8400-e29b-41d4-a716-446655440000";
346 let range = finder.find(input).unwrap();
347 assert_eq!("550e8400-e29b-41d4-a716-446655440000", &input[range]);
348 }
349
350 #[test]
351 fn find_should_handle_input_shorter_than_uuid() {
352 let finder = Uuid::default();
353 assert!(finder.find("abc").is_none());
354 assert!(finder.find("a").is_none());
355 }
356}
357
358#[cfg(test)]
359mod pattern_tests {
360 use super::Uuid;
361
362 #[test]
363 fn vector_pattern_check_agrees_with_the_scalar_one() {
364 let mut seed = 0x9E37_79B9_7F4A_7C15u64;
365 let mut next = || {
366 seed ^= seed << 13;
367 seed ^= seed >> 7;
368 seed ^= seed << 17;
369 seed
370 };
371 let alphabet = b"0123456789abcdefABCDEF-gG/ :";
372 for round in 0..20_000 {
373 let mut bytes = *b"550e8400-e29b-41d4-a716-446655440000";
374 let flips = round % 4;
376 for _ in 0..flips {
377 let at = (next() % 36) as usize;
378 bytes[at] = alphabet[(next() % alphabet.len() as u64) as usize];
379 }
380 let input = bytes.to_vec();
381 assert_eq!(
382 Uuid::check_pattern(&input, 0),
383 Uuid::check_pattern_scalar(&bytes),
384 "{}",
385 String::from_utf8_lossy(&bytes)
386 );
387 }
388 }
389}