Skip to main content

freeswitch_log_parser/fields/
collect.rs

1//! Locating the spans a message carries, by re-running the isolation each
2//! classification arm already performed.
3
4use std::net::IpAddr;
5use std::ops::Range;
6use std::str::FromStr;
7
8use freeswitch_types::ChannelVariable;
9
10use crate::message::{
11    classify_message, dialplan_parts, execute_parts, hangup_channel, is_channel_variable_narration,
12    new_channel_name, paren_channel, parse_bracketed_value, regex_condition_parts,
13    set_export_parts, sip_invite_direction, strip_channel_prefix, MessageKind, SipInviteDirection,
14};
15use crate::uuid::find_uuids;
16
17use super::kind::{kind_rank, Field, FieldKind, FieldLocation};
18use super::processing::processing_parts;
19use super::subslice_range;
20
21/// The address inside a `[...]`-bracketed literal, when valid. Shared by
22/// `channel_host_ip` and `invite_source_addr`, whose unbracketed fallbacks
23/// differ (see their doc comments) and stay separate.
24fn bracketed_ip(token: &str) -> Option<&str> {
25    let inner = token.strip_prefix('[')?;
26    let close = inner.find(']')?;
27    let addr = &inner[..close];
28    addr.parse::<IpAddr>().ok().map(|_| addr)
29}
30
31/// The host of a channel name, when it is a literal address rather than a
32/// hostname. Handles the bracketed IPv6 form and a trailing port.
33fn channel_host_ip(channel: &str) -> Option<&str> {
34    let host = channel.rsplit_once('@')?.1;
35
36    if host.starts_with('[') {
37        return bracketed_ip(host);
38    }
39
40    if host.parse::<IpAddr>().is_ok() {
41        return Some(host);
42    }
43
44    // An unbracketed host with a port: only IPv4 can be split this way, since a
45    // bare IPv6 address is all colons.
46    let addr = host.rsplit_once(':')?.0;
47    addr.parse::<std::net::Ipv4Addr>().ok().map(|_| addr)
48}
49
50/// The source address of a `receiving invite from <addr>:<port>` line.
51///
52/// Unlike `channel_host_ip`, the unbracketed form always strips a trailing
53/// `:port` before parsing (SIP always logs source as `addr:port`), so a bare
54/// unbracketed IPv6 host — all colons, no port to strip — is not accepted here.
55fn invite_source_addr(rest: &str) -> Option<&str> {
56    let after = rest.split_once("receiving invite from ")?.1;
57    let token = after.split_whitespace().next()?;
58
59    if token.starts_with('[') {
60        return bracketed_ip(token);
61    }
62
63    let addr = token.rsplit_once(':').map(|(a, _)| a).unwrap_or(token);
64    addr.parse::<IpAddr>().ok().map(|_| addr)
65}
66/// Locate the fields a message carries, as ranges into `msg`.
67///
68/// Pure and stateless — usable on a [`RawLine::message`](crate::RawLine) without
69/// the stream parser. Every range is at [`FieldLocation::Message`]; see
70/// [`LogEntry::fields`](crate::LogEntry::fields) for an entry's attached lines.
71///
72/// Spans are ordered by start ascending then by width descending, so a
73/// containing span always precedes the spans inside it. Ranges are never empty,
74/// and never partially overlap.
75pub fn message_fields(msg: &str) -> Vec<Field> {
76    let mut out = Vec::new();
77    collect_typed(msg, &mut out);
78
79    // The generic scan runs last: a UUID covered by a kind that names its shape
80    // adds nothing — but a value slot names none, so a UUID nests inside it.
81    for (start, uuid) in find_uuids(msg) {
82        let range = start..start + uuid.len();
83        let covered = out
84            .iter()
85            .any(|f: &Field| f.kind != FieldKind::VariableValue && intersects(&f.range, &range));
86        if !covered {
87            push(&mut out, FieldKind::Uuid, range);
88        }
89    }
90
91    sort_spans(&mut out);
92    out
93}
94
95/// Order by start ascending then width descending, so a containing span
96/// always precedes the spans inside it.
97fn sort_spans(fields: &mut [Field]) {
98    fields.sort_by(|a, b| {
99        (
100            a.range.start,
101            std::cmp::Reverse(a.range.end),
102            kind_rank(a.kind),
103        )
104            .cmp(&(
105                b.range.start,
106                std::cmp::Reverse(b.range.end),
107                kind_rank(b.kind),
108            ))
109    });
110}
111
112fn intersects(a: &Range<usize>, b: &Range<usize>) -> bool {
113    a.start < b.end && b.start < a.end
114}
115
116/// Locate the fields of one raw physical line — an attached line, prefix and all.
117///
118/// `parse_line`'s message is always a suffix of the line it parsed, so the
119/// header width is the length difference; deriving it that way keeps the
120/// branches that hand back a `""` literal harmless, where taking the message's
121/// address would not.
122pub(super) fn raw_line_fields(line: &str, at: FieldLocation) -> Vec<Field> {
123    let message = crate::line::parse_line(line).message;
124    debug_assert!(line.ends_with(message), "message is a suffix of its line");
125    let offset = line.len() - message.len();
126
127    let mut out: Vec<Field> = message_fields(message)
128        .into_iter()
129        .map(|f| Field {
130            kind: f.kind,
131            at,
132            range: f.range.start + offset..f.range.end + offset,
133        })
134        .collect();
135
136    // The session UUID sits in the header the message excludes.
137    for (start, uuid) in find_uuids(&line[..offset]) {
138        out.push(Field {
139            kind: FieldKind::Uuid,
140            at,
141            range: start..start + uuid.len(),
142        });
143    }
144
145    sort_spans(&mut out);
146    out
147}
148fn push(out: &mut Vec<Field>, kind: FieldKind, range: Range<usize>) {
149    if !range.is_empty() {
150        out.push(Field {
151            kind,
152            at: FieldLocation::Message,
153            range,
154        });
155    }
156}
157
158/// Emit a channel name plus the address nested in it, when it has one.
159fn push_channel(out: &mut Vec<Field>, msg: &str, channel: &str) {
160    let Some(range) = subslice_range(msg, channel) else {
161        return;
162    };
163    if let Some(host) = channel_host_ip(channel).and_then(|h| subslice_range(msg, h)) {
164        push(out, FieldKind::IpAddr, host);
165    }
166    push(out, FieldKind::ChannelName, range);
167}
168
169/// The slot a variable's name names; a name outside the identity vocabulary
170/// falls to the neutral value slot rather than going unspanned.
171fn variable_value_kind(name: &str) -> FieldKind {
172    let bare = name.strip_prefix("variable_").unwrap_or(name);
173    match ChannelVariable::from_str(bare) {
174        Ok(ChannelVariable::CallerIdName)
175        | Ok(ChannelVariable::EffectiveCallerIdName)
176        | Ok(ChannelVariable::OriginationCallerIdName) => FieldKind::CallerIdName,
177        Ok(ChannelVariable::CallerIdNumber)
178        | Ok(ChannelVariable::EffectiveCallerIdNumber)
179        | Ok(ChannelVariable::OriginationCallerIdNumber) => FieldKind::CallerIdNumber,
180        Ok(ChannelVariable::DestinationNumber) => FieldKind::DestinationNumber,
181        _ => FieldKind::VariableValue,
182    }
183}
184
185/// The channel and value spans of a Variable-classified message, re-running the
186/// isolation of whichever shape classify_message read it as.
187fn collect_variable(msg: &str, name: &str, out: &mut Vec<Field>) {
188    let push_value = |out: &mut Vec<Field>, value: &str| {
189        if let Some(range) = subslice_range(msg, value) {
190            push(out, variable_value_kind(name), range);
191        }
192    };
193    if msg.starts_with("variable_") {
194        if let Some((_, value)) = parse_bracketed_value(msg, 0) {
195            push_value(out, value);
196        }
197        return;
198    }
199    if let Some((channel, rest)) = strip_channel_prefix(msg) {
200        if is_channel_variable_narration(rest) {
201            if let Some(parts) = set_export_parts(rest) {
202                push_channel(out, msg, channel);
203                push_value(out, parts.value);
204            }
205        }
206        return;
207    }
208    if msg.starts_with("SET ")
209        || msg.starts_with("EXPORT ")
210        || msg.starts_with("PUSH ")
211        || msg.starts_with("UNSHIFT ")
212    {
213        if let Some(parts) = set_export_parts(msg) {
214            if let Some(channel) = parts.channel {
215                push_channel(out, msg, channel);
216            }
217            push_value(out, parts.value);
218        }
219        return;
220    }
221    if let Some(rest) = msg.strip_prefix("CoreSession::setVariable(") {
222        if let Some(inner) = rest.strip_suffix(')') {
223            if let Some(comma) = inner.find(", ") {
224                push_value(out, &inner[comma + 2..]);
225            }
226        }
227        return;
228    }
229    if let Some(rest) = msg.strip_prefix("set variable ") {
230        if let Some((_, value)) = rest.split_once('=') {
231            push_value(out, value);
232        }
233    }
234}
235
236fn collect_typed(msg: &str, out: &mut Vec<Field>) {
237    // Dispatch is classify_message's; this only re-runs the isolation each arm
238    // already performed, to recover the offsets it dropped.
239    match classify_message(msg) {
240        MessageKind::Execute { .. } => push_channel(out, msg, execute_parts(msg).channel),
241        MessageKind::Dialplan { .. } => collect_dialplan(msg, out),
242        MessageKind::Variable { name, .. } => collect_variable(msg, &name, out),
243        MessageKind::ChannelField { name, .. } => collect_channel_field(msg, &name, out),
244        MessageKind::SipInvite { direction, .. } => collect_invite(msg, direction, out),
245        MessageKind::StateChange { .. } | MessageKind::Media { .. } => {
246            collect_channel_prefixed(msg, out);
247        }
248        MessageKind::ChannelLifecycle { .. } if !collect_channel_prefixed(msg, out) => {
249            if let Some(channel) = hangup_channel(msg).or_else(|| new_channel_name(msg)) {
250                push_channel(out, msg, channel);
251            }
252        }
253        _ => {}
254    }
255}
256
257/// The channel token of a `sofia/...`-prefixed line. Returns whether one was found.
258fn collect_channel_prefixed(msg: &str, out: &mut Vec<Field>) -> bool {
259    match strip_channel_prefix(msg) {
260        Some((channel, _)) => {
261            push_channel(out, msg, channel);
262            true
263        }
264        None => match paren_channel(msg) {
265            Some(channel) => {
266                push_channel(out, msg, channel);
267                true
268            }
269            None => false,
270        },
271    }
272}
273
274fn collect_dialplan(msg: &str, out: &mut Vec<Field>) {
275    if msg.starts_with("Dialplan: ") || msg.starts_with("Chatplan: ") {
276        let (channel, detail) = dialplan_parts(msg);
277        push_channel(out, msg, channel);
278        // A head the slicer cannot read yields no span: guessing at one is what
279        // the backstop is for.
280        if let Some(cond) = regex_condition_parts(detail) {
281            if let Some(range) = subslice_range(msg, cond.value) {
282                push(out, variable_value_kind(cond.field), range);
283            }
284        }
285        return;
286    }
287    // The bracketless `from->to` shape names no display name, so `head` stays
288    // unclassified rather than being guessed at.
289    if let Some(parts) = processing_parts(msg) {
290        if let Some(name) = parts.name {
291            push(out, FieldKind::CallerIdName, name);
292        }
293        if let Some(number) = parts.number {
294            push(out, FieldKind::CallerIdNumber, number);
295        }
296        push(out, FieldKind::DestinationNumber, parts.dest);
297    }
298}
299
300fn collect_channel_field(msg: &str, name: &str, out: &mut Vec<Field>) {
301    let kind = match name {
302        "Channel-Name" => FieldKind::ChannelName,
303        "Caller-Caller-ID-Name" => FieldKind::CallerIdName,
304        "Caller-Caller-ID-Number" => FieldKind::CallerIdNumber,
305        "Caller-Destination-Number" => FieldKind::DestinationNumber,
306        _ => FieldKind::VariableValue,
307    };
308    let Some((_, value)) = parse_bracketed_value(msg, 0) else {
309        return;
310    };
311    if kind == FieldKind::ChannelName {
312        push_channel(out, msg, value);
313    } else if let Some(range) = subslice_range(msg, value) {
314        push(out, kind, range);
315    }
316}
317
318fn collect_invite(msg: &str, direction: SipInviteDirection, out: &mut Vec<Field>) {
319    let Some((channel, rest)) = strip_channel_prefix(msg) else {
320        return;
321    };
322    push_channel(out, msg, channel);
323
324    if sip_invite_direction(rest).is_none() {
325        return;
326    }
327    if let Some(range) = crate::message::call_id_token(rest).and_then(|t| subslice_range(msg, t)) {
328        push(out, FieldKind::CallId, range);
329    }
330    if direction == SipInviteDirection::Receiving {
331        if let Some(range) = invite_source_addr(rest).and_then(|a| subslice_range(msg, a)) {
332            push(out, FieldKind::IpAddr, range);
333        }
334    }
335}