Skip to main content

freeswitch_sofia_trace_parser/
types.rs

1use std::borrow::Cow;
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum SkipReason {
6    PartialFirstFrame,
7    OversizedFrame,
8    MidStreamSkip,
9    ReplayedFrame,
10    IncompleteFrame,
11    InvalidHeader,
12}
13
14impl fmt::Display for SkipReason {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            SkipReason::PartialFirstFrame => f.write_str("partial first frame"),
18            SkipReason::OversizedFrame => f.write_str("oversized frame"),
19            SkipReason::MidStreamSkip => f.write_str("mid-stream skip"),
20            SkipReason::ReplayedFrame => f.write_str("replayed frame (logrotate)"),
21            SkipReason::IncompleteFrame => f.write_str("incomplete frame"),
22            SkipReason::InvalidHeader => f.write_str("invalid header"),
23        }
24    }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SkipTracking {
29    CountOnly,
30    TrackRegions,
31    CaptureData,
32}
33
34#[derive(Debug, Clone)]
35pub struct UnparsedRegion {
36    pub offset: u64,
37    pub length: u64,
38    pub reason: SkipReason,
39    pub data: Option<Vec<u8>>,
40}
41
42#[derive(Debug, Default, Clone)]
43pub struct ParseStats {
44    pub bytes_read: u64,
45    pub bytes_skipped: u64,
46    pub unparsed_regions: Vec<UnparsedRegion>,
47}
48
49impl ParseStats {
50    pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
51        std::mem::take(&mut self.unparsed_regions)
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum Direction {
57    Recv,
58    Sent,
59}
60
61impl fmt::Display for Direction {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Direction::Recv => f.write_str("recv"),
65            Direction::Sent => f.write_str("sent"),
66        }
67    }
68}
69
70impl Direction {
71    pub fn preposition(&self) -> &'static str {
72        match self {
73            Direction::Recv => "from",
74            Direction::Sent => "to",
75        }
76    }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Transport {
81    Tcp,
82    Udp,
83    Tls,
84    Wss,
85}
86
87impl fmt::Display for Transport {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Transport::Tcp => f.write_str("tcp"),
91            Transport::Udp => f.write_str("udp"),
92            Transport::Tls => f.write_str("tls"),
93            Transport::Wss => f.write_str("wss"),
94        }
95    }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Timestamp {
100    TimeOnly {
101        hour: u8,
102        min: u8,
103        sec: u8,
104        usec: u32,
105    },
106    DateTime {
107        year: u16,
108        month: u8,
109        day: u8,
110        hour: u8,
111        min: u8,
112        sec: u8,
113        usec: u32,
114    },
115}
116
117impl Timestamp {
118    pub fn time_of_day_secs(&self) -> u32 {
119        let (h, m, s) = match self {
120            Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
121            Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
122        };
123        h as u32 * 3600 + m as u32 * 60 + s as u32
124    }
125
126    pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
127        match self {
128            Timestamp::TimeOnly {
129                hour,
130                min,
131                sec,
132                usec,
133            } => (0, 0, 0, *hour, *min, *sec, *usec),
134            Timestamp::DateTime {
135                year,
136                month,
137                day,
138                hour,
139                min,
140                sec,
141                usec,
142            } => (*year, *month, *day, *hour, *min, *sec, *usec),
143        }
144    }
145}
146
147impl fmt::Display for Timestamp {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Timestamp::TimeOnly {
151                hour,
152                min,
153                sec,
154                usec,
155            } => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
156            Timestamp::DateTime {
157                year,
158                month,
159                day,
160                hour,
161                min,
162                sec,
163                usec,
164            } => write!(
165                f,
166                "{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
167            ),
168        }
169    }
170}
171
172#[derive(Debug, Clone)]
173pub struct Frame {
174    pub direction: Direction,
175    pub byte_count: usize,
176    pub transport: Transport,
177    pub address: String,
178    pub timestamp: Timestamp,
179    pub content: Vec<u8>,
180}
181
182#[derive(Debug, Clone)]
183pub struct SipMessage {
184    pub direction: Direction,
185    pub transport: Transport,
186    pub address: String,
187    pub timestamp: Timestamp,
188    pub content: Vec<u8>,
189    pub frame_count: usize,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum SipMessageType {
194    Request { method: String, uri: String },
195    Response { code: u16, reason: String },
196}
197
198impl fmt::Display for SipMessageType {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
202            SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
203        }
204    }
205}
206
207impl SipMessageType {
208    pub fn summary(&self) -> Cow<'_, str> {
209        match self {
210            SipMessageType::Request { method, .. } => Cow::Borrowed(method),
211            SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
212        }
213    }
214}
215
216#[derive(Debug, Clone)]
217pub struct ParsedSipMessage {
218    pub direction: Direction,
219    pub transport: Transport,
220    pub address: String,
221    pub timestamp: Timestamp,
222    pub message_type: SipMessageType,
223    pub headers: Vec<(String, String)>,
224    pub body: Vec<u8>,
225    pub frame_count: usize,
226}
227
228#[derive(Debug, Clone)]
229pub struct MimePart {
230    pub headers: Vec<(String, String)>,
231    pub body: Vec<u8>,
232}
233
234impl MimePart {
235    pub fn content_type(&self) -> Option<&str> {
236        self.headers
237            .iter()
238            .find(|(k, _)| k.eq_ignore_ascii_case("Content-Type"))
239            .map(|(_, v)| v.as_str())
240    }
241
242    fn header_value(&self, name: &str) -> Option<&str> {
243        let name_lower = name.to_ascii_lowercase();
244        self.headers
245            .iter()
246            .find(|(k, _)| k.to_ascii_lowercase() == name_lower)
247            .map(|(_, v)| v.as_str())
248    }
249
250    pub fn content_id(&self) -> Option<&str> {
251        self.header_value("Content-ID")
252    }
253
254    pub fn content_disposition(&self) -> Option<&str> {
255        self.header_value("Content-Disposition")
256    }
257}
258
259impl ParsedSipMessage {
260    pub fn call_id(&self) -> Option<&str> {
261        self.header_value("Call-ID")
262            .or_else(|| self.header_value("i"))
263    }
264
265    pub fn content_type(&self) -> Option<&str> {
266        self.header_value("Content-Type")
267            .or_else(|| self.header_value("c"))
268    }
269
270    pub fn content_length(&self) -> Option<usize> {
271        self.header_value("Content-Length")
272            .or_else(|| self.header_value("l"))
273            .and_then(|v| v.trim().parse().ok())
274    }
275
276    pub fn cseq(&self) -> Option<&str> {
277        self.header_value("CSeq")
278    }
279
280    pub fn method(&self) -> Option<&str> {
281        match &self.message_type {
282            SipMessageType::Request { method, .. } => Some(method),
283            SipMessageType::Response { .. } => {
284                self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
285            }
286        }
287    }
288
289    pub fn body_data(&self) -> Cow<'_, str> {
290        String::from_utf8_lossy(&self.body)
291    }
292
293    pub fn to_bytes(&self) -> Vec<u8> {
294        let mut out = Vec::new();
295        match &self.message_type {
296            SipMessageType::Request { method, uri } => {
297                out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
298            }
299            SipMessageType::Response { code, reason } => {
300                out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
301            }
302        }
303        for (name, value) in &self.headers {
304            out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
305        }
306        out.extend_from_slice(b"\r\n");
307        if !self.body.is_empty() {
308            out.extend_from_slice(&self.body);
309        }
310        out
311    }
312
313    fn header_value(&self, name: &str) -> Option<&str> {
314        let name_lower = name.to_ascii_lowercase();
315        self.headers
316            .iter()
317            .find(|(k, _)| k.to_ascii_lowercase() == name_lower)
318            .map(|(_, v)| v.as_str())
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    fn make_parsed(
327        msg_type: SipMessageType,
328        headers: Vec<(&str, &str)>,
329        body: &[u8],
330    ) -> ParsedSipMessage {
331        ParsedSipMessage {
332            direction: Direction::Recv,
333            transport: Transport::Tcp,
334            address: "10.0.0.1:5060".into(),
335            timestamp: Timestamp::TimeOnly {
336                hour: 12,
337                min: 0,
338                sec: 0,
339                usec: 0,
340            },
341            message_type: msg_type,
342            headers: headers
343                .iter()
344                .map(|(k, v)| (k.to_string(), v.to_string()))
345                .collect(),
346            body: body.to_vec(),
347            frame_count: 1,
348        }
349    }
350
351    #[test]
352    fn to_bytes_request_no_body() {
353        let msg = make_parsed(
354            SipMessageType::Request {
355                method: "OPTIONS".into(),
356                uri: "sip:host".into(),
357            },
358            vec![("Call-ID", "test")],
359            b"",
360        );
361        let bytes = msg.to_bytes();
362        let text = String::from_utf8(bytes).unwrap();
363        assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
364        assert!(text.contains("Call-ID: test\r\n"));
365        assert!(text.ends_with("\r\n\r\n"));
366    }
367
368    #[test]
369    fn to_bytes_request_with_body() {
370        let body = b"v=0\r\ns=-\r\n";
371        let msg = make_parsed(
372            SipMessageType::Request {
373                method: "INVITE".into(),
374                uri: "sip:host".into(),
375            },
376            vec![("Call-ID", "test")],
377            body,
378        );
379        let bytes = msg.to_bytes();
380        assert!(bytes.ends_with(body));
381    }
382
383    #[test]
384    fn to_bytes_response() {
385        let msg = make_parsed(
386            SipMessageType::Response {
387                code: 200,
388                reason: "OK".into(),
389            },
390            vec![("Call-ID", "resp-test")],
391            b"",
392        );
393        let bytes = msg.to_bytes();
394        let text = String::from_utf8(bytes).unwrap();
395        assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
396    }
397
398    #[test]
399    fn body_data_valid_utf8() {
400        let msg = make_parsed(
401            SipMessageType::Request {
402                method: "MESSAGE".into(),
403                uri: "sip:host".into(),
404            },
405            vec![],
406            b"hello world",
407        );
408        assert_eq!(&*msg.body_data(), "hello world");
409    }
410
411    #[test]
412    fn body_data_empty() {
413        let msg = make_parsed(
414            SipMessageType::Request {
415                method: "OPTIONS".into(),
416                uri: "sip:host".into(),
417            },
418            vec![],
419            b"",
420        );
421        assert_eq!(&*msg.body_data(), "");
422    }
423
424    #[test]
425    fn body_data_binary() {
426        let msg = make_parsed(
427            SipMessageType::Request {
428                method: "MESSAGE".into(),
429                uri: "sip:host".into(),
430            },
431            vec![],
432            &[0xFF, 0xFE],
433        );
434        assert!(msg.body_data().contains('\u{FFFD}'));
435    }
436
437    #[test]
438    fn body_text_non_json_passthrough() {
439        let msg = make_parsed(
440            SipMessageType::Request {
441                method: "INVITE".into(),
442                uri: "sip:host".into(),
443            },
444            vec![("Content-Type", "application/sdp")],
445            b"v=0\r\ns=-\r\n",
446        );
447        assert_eq!(msg.body_text().as_ref(), msg.body_data().as_ref());
448    }
449
450    #[test]
451    fn body_text_json_unescapes_newlines() {
452        let msg = make_parsed(
453            SipMessageType::Request {
454                method: "NOTIFY".into(),
455                uri: "sip:host".into(),
456            },
457            vec![("Content-Type", "application/json")],
458            br#"{"invite":"INVITE sip:host SIP/2.0\r\nTo: <sip:host>\r\n"}"#,
459        );
460        let text = msg.body_text();
461        assert!(
462            text.contains("INVITE sip:host SIP/2.0\r\nTo: <sip:host>\r\n"),
463            "JSON \\r\\n should be unescaped to actual CRLF, got: {text:?}"
464        );
465    }
466
467    #[test]
468    fn body_text_plus_json_content_type() {
469        let msg = make_parsed(
470            SipMessageType::Request {
471                method: "NOTIFY".into(),
472                uri: "sip:host".into(),
473            },
474            vec![(
475                "Content-Type",
476                "application/emergencyCallData.AbandonedCall+json",
477            )],
478            br#"{"invite":"line1\nline2"}"#,
479        );
480        let text = msg.body_text();
481        assert!(
482            text.contains("line1\nline2"),
483            "application/*+json should trigger unescaping, got: {text:?}"
484        );
485    }
486
487    #[test]
488    fn body_data_preserves_json_escapes() {
489        let raw = br#"{"key":"value\nwith\\escapes"}"#;
490        let msg = make_parsed(
491            SipMessageType::Request {
492                method: "NOTIFY".into(),
493                uri: "sip:host".into(),
494            },
495            vec![("Content-Type", "application/json")],
496            raw,
497        );
498        assert_eq!(
499            msg.body_data().as_ref(),
500            r#"{"key":"value\nwith\\escapes"}"#,
501            "body_data() must preserve raw escapes"
502        );
503    }
504}