Skip to main content

freeswitch_log_parser/
uuid.rs

1//! Positional UUID recognition, for callers that need to find a channel UUID
2//! somewhere other than a line's session prefix — inside a message body, a
3//! channel-variable value, or an operator-supplied search needle.
4
5use crate::line::{is_uuid_body_at, UUID_LEN};
6
7/// Whether `s` is exactly one canonical UUID: 8-4-4-4-12 hex digits, either case.
8pub fn is_uuid(s: &str) -> bool {
9    s.len() == UUID_LEN && is_uuid_body_at(s.as_bytes(), 0)
10}
11
12/// Iterate the UUIDs embedded anywhere in `text`, yielding each match's byte
13/// offset and slice. Matches never overlap.
14pub fn find_uuids(text: &str) -> FindUuids<'_> {
15    FindUuids { text, pos: 0 }
16}
17
18/// Iterator returned by [`find_uuids`].
19#[derive(Debug, Clone)]
20pub struct FindUuids<'a> {
21    text: &'a str,
22    pos: usize,
23}
24
25impl<'a> Iterator for FindUuids<'a> {
26    type Item = (usize, &'a str);
27
28    fn next(&mut self) -> Option<(usize, &'a str)> {
29        let bytes = self.text.as_bytes();
30        while self.pos + UUID_LEN <= bytes.len() {
31            let start = self.pos;
32            // Every UUID byte is ASCII, so `start` is always a char boundary and
33            // the slice below cannot split a codepoint.
34            if is_uuid_body_at(bytes, start) {
35                self.pos = start + UUID_LEN;
36                return Some((start, &self.text[start..self.pos]));
37            }
38            self.pos += 1;
39        }
40        None
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    const A: &str = "11111111-2222-3333-4444-555555555555";
49    const B: &str = "aaaabbbb-cccc-dddd-eeee-ffff00001111";
50
51    fn found(text: &str) -> Vec<&str> {
52        find_uuids(text).map(|(_, u)| u).collect()
53    }
54
55    #[test]
56    fn is_uuid_accepts_either_case() {
57        assert!(is_uuid(A));
58        assert!(is_uuid("AAAABBBB-2222-3333-4444-5555CCCCDDDD"));
59    }
60
61    #[test]
62    fn is_uuid_rejects_partial_and_trailing() {
63        assert!(!is_uuid(&A[..35]));
64        assert!(!is_uuid(&format!("{A}0")));
65        assert!(!is_uuid("11111111_2222-3333-4444-555555555555"));
66        assert!(!is_uuid("gggggggg-2222-3333-4444-555555555555"));
67    }
68
69    #[test]
70    fn finds_uuid_at_end_of_string() {
71        assert_eq!(found(&format!("Peer UUID: {A}")), vec![A]);
72    }
73
74    #[test]
75    fn finds_uuid_abutting_punctuation() {
76        assert_eq!(found(&format!("<{A}>;tag=x")), vec![A]);
77    }
78
79    #[test]
80    fn finds_uppercase_hex() {
81        let upper = "AAAABBBB-2222-3333-4444-5555CCCCDDDD";
82        assert_eq!(found(&format!("+OK {upper}")), vec![upper]);
83    }
84
85    #[test]
86    fn finds_back_to_back_uuids() {
87        assert_eq!(found(&format!("{A}{B}")), vec![A, B]);
88        assert_eq!(found(&format!("+OK {A}\n{B}")), vec![A, B]);
89    }
90
91    #[test]
92    fn reports_byte_offsets() {
93        let text = format!("Peer UUID: {A}");
94        let hits: Vec<usize> = find_uuids(&text).map(|(at, _)| at).collect();
95        assert_eq!(hits, vec![11]);
96    }
97
98    #[test]
99    fn offsets_survive_multibyte_text() {
100        let text = format!("café {A}");
101        let (at, uuid) = find_uuids(&text).next().unwrap();
102        assert_eq!(uuid, A);
103        assert_eq!(&text[at..], A);
104    }
105
106    #[test]
107    fn no_match_in_plain_text() {
108        assert!(found("no identifiers here at all").is_empty());
109        assert!(found("deadbeef-dead-beef-dead").is_empty());
110    }
111
112    #[test]
113    fn a_longer_hex_run_yields_its_leading_uuid() {
114        // Substring semantics, not tokenization: nothing here delimits a UUID, so
115        // the first 36 conforming bytes are the match.
116        assert_eq!(found(&format!("{A}99")), vec![A]);
117    }
118}