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::mask::Mask;
6
7/// Length of a session UUID in canonical 8-4-4-4-12 hex form.
8pub(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
16/// Length of a UUID followed by its trailing space — the prefix
17/// `mod_logfile` prepends to every line when `log_uuid=true`.
18pub(crate) const UUID_PREFIX_LEN: usize = UUID_LEN + 1;
19
20/// How far into a collided line a session prefix is still looked for. Past it
21/// the fragment ahead of the UUID is data, not a cut token, and Layer 2's
22/// write budget is what finds the boundary.
23const MAX_COLLISION_PREFIX: usize = 50;
24
25/// The 36 canonical UUID bytes at `offset`, with nothing required after them —
26/// an embedded UUID can end a line or abut punctuation.
27pub(crate) fn is_uuid_body_at(bytes: &[u8], offset: usize) -> bool {
28    UUID_MASK.matches_at(bytes, offset)
29}
30
31/// A UUID at `offset` acting as a line's session prefix: the space delimiter is
32/// required, so a message that merely starts with hex is not mistaken for one.
33pub(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
37/// The offset of a session prefix that a buffer collision pushed off the start
38/// of the line.
39pub(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
47/// Whether `s` is exactly one canonical UUID: 8-4-4-4-12 hex digits, either case.
48pub fn is_uuid(s: &str) -> bool {
49    s.len() == UUID_LEN && is_uuid_body_at(s.as_bytes(), 0)
50}
51
52/// Iterate the UUIDs embedded anywhere in `text`, yielding each match's byte
53/// offset and slice. Matches never overlap.
54pub fn find_uuids(text: &str) -> FindUuids<'_> {
55    FindUuids { text, pos: 0 }
56}
57
58/// Iterator returned by [`find_uuids`].
59#[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            // Every UUID byte is ASCII, so `start` is always a char boundary and
73            // the slice below cannot split a codepoint.
74            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        // Substring semantics, not tokenization: nothing here delimits a UUID, so
155        // the first 36 conforming bytes are the match.
156        assert_eq!(found(&format!("{A}99")), vec![A]);
157    }
158}