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