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/// The channel token of a `New Channel <channel> [uuid]` line.
172pub(crate) fn new_channel_name(msg: &str) -> Option<&str> {
173    let rest = msg.strip_prefix("New Channel ")?;
174    let bracket = rest.rfind(" [")?;
175    Some(&rest[..bracket]).filter(|c| !c.is_empty())
176}
177
178pub(crate) fn strip_channel_prefix(msg: &str) -> Option<(&str, &str)> {
179    if !msg.starts_with("sofia/") && !msg.starts_with("loopback/") {
180        return None;
181    }
182    let bytes = msg.as_bytes();
183    let mut i = 0;
184    while i < bytes.len() {
185        match bytes[i] {
186            b'[' => i = matching_close(bytes, i + 1, b'[', b']')?,
187            b' ' => return Some((&msg[..i], &msg[i + 1..])),
188            _ => {}
189        }
190        i += 1;
191    }
192    None
193}
194/// The subslices a `SET`/`EXPORT` line decomposes into. `channel` is `None` for
195/// `EXPORT`, which names no channel.
196pub(crate) struct SetExportParts<'a> {
197    pub(crate) channel: Option<&'a str>,
198    pub(crate) name: &'a str,
199    pub(crate) value: &'a str,
200}
201
202pub(crate) fn set_export_parts(msg: &str) -> Option<SetExportParts<'_>> {
203    // `]=[` is the one token every narration of this shape shares, whatever
204    // precedes it.
205    let sep_pos = msg.find("]=[")?;
206    let name_start = msg[..sep_pos].rfind('[')?;
207    let name = &msg[name_start + 1..sep_pos];
208    let val_start = sep_pos + 3; // skip "]=["
209    let val_end = matching_close(msg.as_bytes(), val_start, b'[', b']').unwrap_or(msg.len());
210    let value = &msg[val_start..val_end];
211
212    // Only verb-first shapes name a channel, and not always: `SET GLOBAL` is a
213    // scope and `SET [n]=[v]` has no token at all — an endpoint path qualifies.
214    let channel = msg
215        .strip_prefix("SET ")
216        .or_else(|| msg.strip_prefix("PUSH "))
217        .or_else(|| msg.strip_prefix("UNSHIFT "))
218        .and_then(|rest| {
219            let end = rest.find(' ').unwrap_or(rest.len());
220            Some(&rest[..end]).filter(|c| c.contains('/') && !c.starts_with('['))
221        });
222
223    Some(SetExportParts {
224        channel,
225        name,
226        value,
227    })
228}