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, EventHeader, VARIABLE_PREFIX};
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::{field_order, 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
95fn sort_spans(fields: &mut [Field]) {
96    fields.sort_by(field_order);
97}
98
99fn intersects(a: &Range<usize>, b: &Range<usize>) -> bool {
100    a.start < b.end && b.start < a.end
101}
102
103/// Locate the fields of one raw physical line — an attached line, prefix and all.
104///
105/// `parse_line`'s message is always a suffix of the line it parsed, so the
106/// header width is the length difference; deriving it that way keeps the
107/// branches that hand back a `""` literal harmless, where taking the message's
108/// address would not.
109pub(super) fn raw_line_fields(line: &str, at: FieldLocation) -> Vec<Field> {
110    let message = crate::line::parse_line(line).message;
111    debug_assert!(line.ends_with(message), "message is a suffix of its line");
112    let offset = line.len() - message.len();
113
114    let mut out: Vec<Field> = message_fields(message)
115        .into_iter()
116        .map(|f| Field {
117            kind: f.kind,
118            at,
119            range: f.range.start + offset..f.range.end + offset,
120        })
121        .collect();
122
123    // The session UUID sits in the header the message excludes.
124    for (start, uuid) in find_uuids(&line[..offset]) {
125        out.push(Field {
126            kind: FieldKind::Uuid,
127            at,
128            range: start..start + uuid.len(),
129        });
130    }
131
132    sort_spans(&mut out);
133    out
134}
135fn push(out: &mut Vec<Field>, kind: FieldKind, range: Range<usize>) {
136    if !range.is_empty() {
137        out.push(Field {
138            kind,
139            at: FieldLocation::Message,
140            range,
141        });
142    }
143}
144
145/// Emit a channel name plus the address nested in it, when it has one.
146fn push_channel(out: &mut Vec<Field>, msg: &str, channel: &str) {
147    let Some(range) = subslice_range(msg, channel) else {
148        return;
149    };
150    if let Some(host) = channel_host_ip(channel).and_then(|h| subslice_range(msg, h)) {
151        push(out, FieldKind::IpAddr, host);
152    }
153    push(out, FieldKind::ChannelName, range);
154}
155
156/// The slot a variable's name names; a name outside the identity vocabulary
157/// falls to the neutral value slot rather than going unspanned.
158fn variable_value_kind(name: &str) -> FieldKind {
159    match ChannelVariable::from_str(name) {
160        Ok(ChannelVariable::CallerIdName)
161        | Ok(ChannelVariable::EffectiveCallerIdName)
162        | Ok(ChannelVariable::OriginationCallerIdName) => FieldKind::CallerIdName,
163        Ok(ChannelVariable::CallerIdNumber)
164        | Ok(ChannelVariable::EffectiveCallerIdNumber)
165        | Ok(ChannelVariable::OriginationCallerIdNumber) => FieldKind::CallerIdNumber,
166        Ok(ChannelVariable::DestinationNumber) => FieldKind::DestinationNumber,
167        _ => FieldKind::VariableValue,
168    }
169}
170
171/// The channel and value spans of a Variable-classified message, re-running the
172/// isolation of whichever shape classify_message read it as.
173fn collect_variable(msg: &str, name: &str, out: &mut Vec<Field>) {
174    let push_value = |out: &mut Vec<Field>, value: &str| {
175        if let Some(range) = subslice_range(msg, value) {
176            push(out, variable_value_kind(name), range);
177        }
178    };
179    if msg.starts_with(VARIABLE_PREFIX) {
180        if let Some((_, value)) = parse_bracketed_value(msg, 0) {
181            push_value(out, value);
182        }
183        return;
184    }
185    if let Some((channel, rest)) = strip_channel_prefix(msg) {
186        if is_channel_variable_narration(rest) {
187            if let Some(parts) = set_export_parts(rest) {
188                push_channel(out, msg, channel);
189                push_value(out, parts.value);
190            }
191        }
192        return;
193    }
194    if msg.starts_with("SET ")
195        || msg.starts_with("EXPORT ")
196        || msg.starts_with("PUSH ")
197        || msg.starts_with("UNSHIFT ")
198    {
199        if let Some(parts) = set_export_parts(msg) {
200            if let Some(channel) = parts.channel {
201                push_channel(out, msg, channel);
202            }
203            push_value(out, parts.value);
204        }
205        return;
206    }
207    if let Some(rest) = msg.strip_prefix("CoreSession::setVariable(") {
208        if let Some(inner) = rest.strip_suffix(')') {
209            if let Some(comma) = inner.find(", ") {
210                push_value(out, &inner[comma + 2..]);
211            }
212        }
213        return;
214    }
215    if let Some(rest) = msg.strip_prefix("set variable ") {
216        if let Some((_, value)) = rest.split_once('=') {
217            push_value(out, value);
218        }
219    }
220}
221
222fn collect_typed(msg: &str, out: &mut Vec<Field>) {
223    // Dispatch is classify_message's; this only re-runs the isolation each arm
224    // already performed, to recover the offsets it dropped.
225    match classify_message(msg) {
226        MessageKind::Execute { .. } => push_channel(out, msg, execute_parts(msg).channel),
227        MessageKind::Dialplan { .. } => collect_dialplan(msg, out),
228        MessageKind::Variable { name, .. } => collect_variable(msg, name.bare(), out),
229        MessageKind::ChannelField { name, .. } => collect_channel_field(msg, &name, out),
230        MessageKind::SipInvite { direction, .. } => collect_invite(msg, direction, out),
231        MessageKind::StateChange { .. } | MessageKind::Media { .. } => {
232            collect_channel_prefixed(msg, out);
233        }
234        MessageKind::ChannelLifecycle { .. } if !collect_channel_prefixed(msg, out) => {
235            if let Some(channel) = hangup_channel(msg).or_else(|| new_channel_name(msg)) {
236                push_channel(out, msg, channel);
237            }
238        }
239        _ => {}
240    }
241}
242
243/// The channel token of a `sofia/...`-prefixed line. Returns whether one was found.
244fn collect_channel_prefixed(msg: &str, out: &mut Vec<Field>) -> bool {
245    match strip_channel_prefix(msg) {
246        Some((channel, _)) => {
247            push_channel(out, msg, channel);
248            true
249        }
250        None => match paren_channel(msg) {
251            Some(channel) => {
252                push_channel(out, msg, channel);
253                true
254            }
255            None => false,
256        },
257    }
258}
259
260fn collect_dialplan(msg: &str, out: &mut Vec<Field>) {
261    if msg.starts_with("Dialplan: ") || msg.starts_with("Chatplan: ") {
262        let (channel, detail) = dialplan_parts(msg);
263        push_channel(out, msg, channel);
264        // A head the slicer cannot read yields no span: guessing at one is what
265        // the backstop is for.
266        if let Some(cond) = regex_condition_parts(detail) {
267            if let Some(range) = subslice_range(msg, cond.value) {
268                push(out, variable_value_kind(cond.field), range);
269            }
270        }
271        return;
272    }
273    // The bracketless `from->to` shape names no display name, so `head` stays
274    // unclassified rather than being guessed at.
275    if let Some(parts) = processing_parts(msg) {
276        if let Some(name) = parts.name {
277            push(out, FieldKind::CallerIdName, name);
278        }
279        if let Some(number) = parts.number {
280            push(out, FieldKind::CallerIdNumber, number);
281        }
282        push(out, FieldKind::DestinationNumber, parts.dest);
283    }
284}
285
286fn collect_channel_field(msg: &str, name: &str, out: &mut Vec<Field>) {
287    let kind = match EventHeader::from_str(name) {
288        Ok(EventHeader::ChannelName) => FieldKind::ChannelName,
289        Ok(EventHeader::CallerCallerIdName) => FieldKind::CallerIdName,
290        Ok(EventHeader::CallerCallerIdNumber) => FieldKind::CallerIdNumber,
291        Ok(EventHeader::CallerDestinationNumber) => FieldKind::DestinationNumber,
292        _ => FieldKind::VariableValue,
293    };
294    let Some((_, value)) = parse_bracketed_value(msg, 0) else {
295        return;
296    };
297    if kind == FieldKind::ChannelName {
298        push_channel(out, msg, value);
299    } else if let Some(range) = subslice_range(msg, value) {
300        push(out, kind, range);
301    }
302}
303
304fn collect_invite(msg: &str, direction: SipInviteDirection, out: &mut Vec<Field>) {
305    let Some((channel, rest)) = strip_channel_prefix(msg) else {
306        return;
307    };
308    push_channel(out, msg, channel);
309
310    if sip_invite_direction(rest).is_none() {
311        return;
312    }
313    if let Some(range) = crate::message::call_id_token(rest).and_then(|t| subslice_range(msg, t)) {
314        push(out, FieldKind::CallId, range);
315    }
316    if direction == SipInviteDirection::Receiving {
317        if let Some(range) = invite_source_addr(rest).and_then(|a| subslice_range(msg, a)) {
318            push(out, FieldKind::IpAddr, range);
319        }
320    }
321}