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