Skip to main content

freeswitch_log_parser/message/
parts.rs

1//! Positional slicers shared by classification and the field-span API — each
2//! returns borrowed subslices of the message so spans stay addressable.
3
4/// The subslices an `EXECUTE`/`Execute` line decomposes into. `channel` is
5/// empty for the lowercase shape, which carries none.
6pub(crate) struct ExecuteParts<'a> {
7    pub(crate) depth: u32,
8    pub(crate) channel: &'a str,
9    pub(crate) application: &'a str,
10    pub(crate) arguments: &'a str,
11}
12
13pub(crate) fn execute_parts(msg: &str) -> ExecuteParts<'_> {
14    let rest = &msg["EXECUTE ".len()..];
15
16    let depth = if rest.starts_with("[depth=") {
17        let end = rest.find(']').unwrap_or(0);
18        if end > 7 {
19            rest[7..end].parse::<u32>().unwrap_or(0)
20        } else {
21            0
22        }
23    } else {
24        return ExecuteParts {
25            depth: 0,
26            channel: "",
27            application: "",
28            arguments: rest,
29        };
30    };
31
32    let after_bracket = rest.find("] ").map(|p| &rest[p + 2..]).unwrap_or("");
33
34    // Lowercase "Execute [depth=N] app(args)" has no channel.
35    // Uppercase "EXECUTE [depth=N] channel app(args)" has channel before app.
36    // Detect by checking if first token contains '(' (app) or '/' (channel path).
37    let (channel, app_part) = match after_bracket.find(' ') {
38        Some(p) => {
39            let first_token = &after_bracket[..p];
40            if first_token.contains('/') {
41                (first_token, &after_bracket[p + 1..])
42            } else {
43                ("", after_bracket)
44            }
45        }
46        None => ("", after_bracket),
47    };
48
49    let (application, arguments) = match app_part.find('(') {
50        Some(p) => {
51            let app = &app_part[..p];
52            let args = if app_part.ends_with(')') {
53                &app_part[p + 1..app_part.len() - 1]
54            } else {
55                &app_part[p + 1..]
56            };
57            (app, args)
58        }
59        None => (app_part, ""),
60    };
61
62    ExecuteParts {
63        depth,
64        channel,
65        application,
66        arguments,
67    }
68}
69/// `(channel, detail)` of a `Dialplan:`/`Chatplan:` line.
70pub(crate) fn dialplan_parts(msg: &str) -> (&str, &str) {
71    let prefix_len = if msg.starts_with("Chatplan: ") {
72        "Chatplan: ".len()
73    } else {
74        "Dialplan: ".len()
75    };
76    let rest = &msg[prefix_len..];
77    match rest.find(' ') {
78        Some(p) => (&rest[..p], &rest[p + 1..]),
79        None => (rest, ""),
80    }
81}
82/// The parts of a dialplan `Regex` condition trace, from [`regex_condition_parts`].
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[non_exhaustive]
85pub struct RegexCondition<'a> {
86    /// The condition's raw `field` attribute, as the dialplan spelled it.
87    pub field: &'a str,
88    /// The expanded value the condition was tested against.
89    pub value: &'a str,
90}
91
92/// The parts of a `Regex (PASS|FAIL) [exten] field(value) =~ /expr/ tail`
93/// trace — [`MessageKind::Dialplan`](crate::MessageKind::Dialplan)'s `detail`
94/// payload; `None` on any other text.
95///
96/// The field is the dialplan's raw attribute, so it may be a `${…}` API call
97/// carrying parentheses of its own: the value's opening paren is matched from
98/// the right, never taken as the first one in the head.
99pub fn regex_condition_parts(detail: &str) -> Option<RegexCondition<'_>> {
100    let after_exten = detail.strip_prefix("Regex (")?.split_once(") [")?.1;
101    let head = &after_exten[after_exten.find("] ")? + 2..];
102    // A value can carry the separator; an expression realistically cannot.
103    let head = &head[..head.rfind(" =~ /")?];
104    let close = head.strip_suffix(')')?.len();
105
106    let bytes = head.as_bytes();
107    let mut depth = 1u32;
108    let mut i = close;
109    while i > 0 {
110        i -= 1;
111        match bytes[i] {
112            b')' => depth += 1,
113            b'(' => {
114                depth -= 1;
115                if depth == 0 {
116                    return Some(RegexCondition {
117                        field: &head[..i],
118                        value: &head[i + 1..close],
119                    });
120                }
121            }
122            _ => {}
123        }
124    }
125    None
126}
127
128pub(crate) fn parse_bracketed_value(s: &str, prefix_len: usize) -> Option<(&str, &str)> {
129    let after_prefix = &s[prefix_len..];
130    let colon = after_prefix.find(": ")?;
131    let name = &after_prefix[..colon];
132    let value_part = &after_prefix[colon + 2..];
133    if let Some(inner) = value_part.strip_prefix('[') {
134        if let Some(stripped) = inner.strip_suffix(']') {
135            Some((name, stripped))
136        } else {
137            Some((name, inner))
138        }
139    } else {
140        Some((name, value_part))
141    }
142}
143/// The channel token of a `(channel) State ...` line, parentheses excluded.
144pub(crate) fn paren_channel(msg: &str) -> Option<&str> {
145    let inner = msg.strip_prefix('(')?;
146    let close = inner.find(')')?;
147    Some(&inner[..close]).filter(|c| !c.is_empty())
148}
149
150/// The channel token of a `Hangup <channel> [state] [cause]` line.
151pub(crate) fn hangup_channel(msg: &str) -> Option<&str> {
152    let rest = msg.strip_prefix("Hangup ")?;
153    let bracket = rest.find(" [")?;
154    Some(&rest[..bracket]).filter(|c| !c.is_empty())
155}
156
157/// The channel token of a `New Channel <channel> [uuid]` line.
158pub(crate) fn new_channel_name(msg: &str) -> Option<&str> {
159    let rest = msg.strip_prefix("New Channel ")?;
160    let bracket = rest.rfind(" [")?;
161    Some(&rest[..bracket]).filter(|c| !c.is_empty())
162}
163
164pub(crate) fn strip_channel_prefix(msg: &str) -> Option<(&str, &str)> {
165    if !msg.starts_with("sofia/") && !msg.starts_with("loopback/") {
166        return None;
167    }
168    let bytes = msg.as_bytes();
169    let mut i = 0;
170    let mut bracket_depth: u32 = 0;
171    while i < bytes.len() {
172        match bytes[i] {
173            b'[' => bracket_depth += 1,
174            b']' => {
175                bracket_depth = bracket_depth.saturating_sub(1);
176            }
177            b' ' if bracket_depth == 0 => {
178                return Some((&msg[..i], &msg[i + 1..]));
179            }
180            _ => {}
181        }
182        i += 1;
183    }
184    None
185}
186/// The subslices a `SET`/`EXPORT` line decomposes into. `channel` is `None` for
187/// `EXPORT`, which names no channel.
188pub(crate) struct SetExportParts<'a> {
189    pub(crate) channel: Option<&'a str>,
190    pub(crate) name: &'a str,
191    pub(crate) value: &'a str,
192}
193
194/// The offset of the `]` closing a value that opened at `from`, matching nested
195/// brackets so a value containing one is not cut at it.
196fn value_close(msg: &str, from: usize) -> Option<usize> {
197    let mut depth = 1u32;
198    for (i, b) in msg.as_bytes().iter().enumerate().skip(from) {
199        match b {
200            b'[' => depth += 1,
201            b']' => {
202                depth -= 1;
203                if depth == 0 {
204                    return Some(i);
205                }
206            }
207            _ => {}
208        }
209    }
210    None
211}
212
213pub(crate) fn set_export_parts(msg: &str) -> Option<SetExportParts<'_>> {
214    // SET|PUSH|UNSHIFT channel [name]=[value]
215    // EXPORT (export_vars) [(REMOTE ONLY) ][name]=[value]
216    // channel EXPORTING[export_vars] [name]=[value] to event|channel
217    // channel setting variable [name]=[value]
218    // Find "]=[" which uniquely identifies the [name]=[value] boundary
219    let sep_pos = msg.find("]=[")?;
220    let name_start = msg[..sep_pos].rfind('[')?;
221    let name = &msg[name_start + 1..sep_pos];
222    let val_start = sep_pos + 3; // skip "]=["
223    let val_end = value_close(msg, val_start).unwrap_or(msg.len());
224    let value = &msg[val_start..val_end];
225
226    // Only verb-first shapes name a channel, and not always: `SET GLOBAL` is a
227    // scope and `SET [n]=[v]` has no token at all — an endpoint path qualifies.
228    let channel = msg
229        .strip_prefix("SET ")
230        .or_else(|| msg.strip_prefix("PUSH "))
231        .or_else(|| msg.strip_prefix("UNSHIFT "))
232        .and_then(|rest| {
233            let end = rest.find(' ').unwrap_or(rest.len());
234            Some(&rest[..end]).filter(|c| c.contains('/') && !c.starts_with('['))
235        });
236
237    Some(SetExportParts {
238        channel,
239        name,
240        value,
241    })
242}