freeswitch_log_parser/
uuid.rs1use crate::mask::Mask;
6
7pub(crate) const UUID_LEN: usize = 36;
9
10const UUID_MASK: Mask = Mask {
11 len: UUID_LEN,
12 separators: &[(8, b'-'), (13, b'-'), (18, b'-'), (23, b'-')],
13 filler: u8::is_ascii_hexdigit,
14};
15
16pub(crate) const UUID_PREFIX_LEN: usize = UUID_LEN + 1;
19
20const MAX_COLLISION_PREFIX: usize = 50;
24
25pub(crate) fn is_uuid_body_at(bytes: &[u8], offset: usize) -> bool {
28 UUID_MASK.matches_at(bytes, offset)
29}
30
31pub(crate) fn is_uuid_at(bytes: &[u8], offset: usize) -> bool {
34 bytes.get(offset + UUID_LEN) == Some(&b' ') && is_uuid_body_at(bytes, offset)
35}
36
37pub(crate) fn find_uuid_in(bytes: &[u8]) -> Option<usize> {
40 if bytes.len() < UUID_PREFIX_LEN {
41 return None;
42 }
43 let max_start = (bytes.len() - UUID_PREFIX_LEN).min(MAX_COLLISION_PREFIX);
44 (1..=max_start).find(|&start| is_uuid_at(bytes, start))
45}
46
47pub fn is_uuid(s: &str) -> bool {
49 s.len() == UUID_LEN && is_uuid_body_at(s.as_bytes(), 0)
50}
51
52pub fn find_uuids(text: &str) -> FindUuids<'_> {
55 FindUuids { text, pos: 0 }
56}
57
58#[derive(Debug, Clone)]
60pub struct FindUuids<'a> {
61 text: &'a str,
62 pos: usize,
63}
64
65impl<'a> Iterator for FindUuids<'a> {
66 type Item = (usize, &'a str);
67
68 fn next(&mut self) -> Option<(usize, &'a str)> {
69 let bytes = self.text.as_bytes();
70 while self.pos + UUID_LEN <= bytes.len() {
71 let start = self.pos;
72 if is_uuid_body_at(bytes, start) {
75 self.pos = start + UUID_LEN;
76 return Some((start, &self.text[start..self.pos]));
77 }
78 self.pos += 1;
79 }
80 None
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 const A: &str = "11111111-2222-3333-4444-555555555555";
89 const B: &str = "aaaabbbb-cccc-dddd-eeee-ffff00001111";
90
91 fn found(text: &str) -> Vec<&str> {
92 find_uuids(text).map(|(_, u)| u).collect()
93 }
94
95 #[test]
96 fn is_uuid_accepts_either_case() {
97 assert!(is_uuid(A));
98 assert!(is_uuid("AAAABBBB-2222-3333-4444-5555CCCCDDDD"));
99 }
100
101 #[test]
102 fn is_uuid_rejects_partial_and_trailing() {
103 assert!(!is_uuid(&A[..35]));
104 assert!(!is_uuid(&format!("{A}0")));
105 assert!(!is_uuid("11111111_2222-3333-4444-555555555555"));
106 assert!(!is_uuid("gggggggg-2222-3333-4444-555555555555"));
107 }
108
109 #[test]
110 fn finds_uuid_at_end_of_string() {
111 assert_eq!(found(&format!("Peer UUID: {A}")), vec![A]);
112 }
113
114 #[test]
115 fn finds_uuid_abutting_punctuation() {
116 assert_eq!(found(&format!("<{A}>;tag=x")), vec![A]);
117 }
118
119 #[test]
120 fn finds_uppercase_hex() {
121 let upper = "AAAABBBB-2222-3333-4444-5555CCCCDDDD";
122 assert_eq!(found(&format!("+OK {upper}")), vec![upper]);
123 }
124
125 #[test]
126 fn finds_back_to_back_uuids() {
127 assert_eq!(found(&format!("{A}{B}")), vec![A, B]);
128 assert_eq!(found(&format!("+OK {A}\n{B}")), vec![A, B]);
129 }
130
131 #[test]
132 fn reports_byte_offsets() {
133 let text = format!("Peer UUID: {A}");
134 let hits: Vec<usize> = find_uuids(&text).map(|(at, _)| at).collect();
135 assert_eq!(hits, vec![11]);
136 }
137
138 #[test]
139 fn offsets_survive_multibyte_text() {
140 let text = format!("café {A}");
141 let (at, uuid) = find_uuids(&text).next().unwrap();
142 assert_eq!(uuid, A);
143 assert_eq!(&text[at..], A);
144 }
145
146 #[test]
147 fn no_match_in_plain_text() {
148 assert!(found("no identifiers here at all").is_empty());
149 assert!(found("deadbeef-dead-beef-dead").is_empty());
150 }
151
152 #[test]
153 fn a_longer_hex_run_yields_its_leading_uuid() {
154 assert_eq!(found(&format!("{A}99")), vec![A]);
157 }
158}