1use std::borrow::Cow;
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SkipReason {
10 PartialFirstFrame,
13 OversizedFrame,
16 MidStreamSkip,
18 ReplayedFrame,
21 IncompleteFrame,
23 InvalidHeader,
25}
26
27impl fmt::Display for SkipReason {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 SkipReason::PartialFirstFrame => f.write_str("partial first frame"),
31 SkipReason::OversizedFrame => f.write_str("oversized frame"),
32 SkipReason::MidStreamSkip => f.write_str("mid-stream skip"),
33 SkipReason::ReplayedFrame => f.write_str("replayed frame (logrotate)"),
34 SkipReason::IncompleteFrame => f.write_str("incomplete frame"),
35 SkipReason::InvalidHeader => f.write_str("invalid header"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum SkipTracking {
46 CountOnly,
48 TrackRegions,
50 CaptureData,
52}
53
54#[derive(Debug, Clone)]
56pub struct UnparsedRegion {
57 pub offset: u64,
59 pub length: u64,
61 pub reason: SkipReason,
63 pub data: Option<Vec<u8>>,
65}
66
67#[derive(Debug, Default, Clone)]
73pub struct ParseStats {
74 pub bytes_read: u64,
76 pub bytes_skipped: u64,
78 pub unparsed_regions: Vec<UnparsedRegion>,
81}
82
83impl ParseStats {
84 pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
86 std::mem::take(&mut self.unparsed_regions)
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum Direction {
93 Recv,
95 Sent,
97}
98
99impl fmt::Display for Direction {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match self {
102 Direction::Recv => f.write_str("recv"),
103 Direction::Sent => f.write_str("sent"),
104 }
105 }
106}
107
108impl Direction {
109 pub fn preposition(&self) -> &'static str {
111 match self {
112 Direction::Recv => "from",
113 Direction::Sent => "to",
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum Transport {
121 Tcp,
123 Udp,
125 Tls,
127 Wss,
129}
130
131impl fmt::Display for Transport {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Transport::Tcp => f.write_str("tcp"),
135 Transport::Udp => f.write_str("udp"),
136 Transport::Tls => f.write_str("tls"),
137 Transport::Wss => f.write_str("wss"),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum Timestamp {
148 TimeOnly {
150 hour: u8,
152 min: u8,
154 sec: u8,
156 usec: u32,
158 },
159 DateTime {
161 year: u16,
163 month: u8,
165 day: u8,
167 hour: u8,
169 min: u8,
171 sec: u8,
173 usec: u32,
175 },
176}
177
178impl Timestamp {
179 pub fn time_of_day_secs(&self) -> u32 {
181 let (h, m, s) = match self {
182 Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
183 Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
184 };
185 h as u32 * 3600 + m as u32 * 60 + s as u32
186 }
187
188 pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
191 match self {
192 Timestamp::TimeOnly {
193 hour,
194 min,
195 sec,
196 usec,
197 } => (0, 0, 0, *hour, *min, *sec, *usec),
198 Timestamp::DateTime {
199 year,
200 month,
201 day,
202 hour,
203 min,
204 sec,
205 usec,
206 } => (*year, *month, *day, *hour, *min, *sec, *usec),
207 }
208 }
209}
210
211impl fmt::Display for Timestamp {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 match self {
214 Timestamp::TimeOnly {
215 hour,
216 min,
217 sec,
218 usec,
219 } => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
220 Timestamp::DateTime {
221 year,
222 month,
223 day,
224 hour,
225 min,
226 sec,
227 usec,
228 } => write!(
229 f,
230 "{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
231 ),
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
242pub struct Frame {
243 pub direction: Direction,
245 pub byte_count: usize,
247 pub transport: Transport,
249 pub address: String,
251 pub timestamp: Timestamp,
253 pub content: Vec<u8>,
255}
256
257#[derive(Debug, Clone)]
262pub struct SipMessage {
263 pub direction: Direction,
265 pub transport: Transport,
267 pub address: String,
269 pub timestamp: Timestamp,
271 pub content: Vec<u8>,
273 pub frame_count: usize,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum SipMessageType {
280 Request {
282 method: String,
284 uri: String,
286 },
287 Response {
289 code: u16,
291 reason: String,
293 },
294}
295
296impl fmt::Display for SipMessageType {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 match self {
299 SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
300 SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
301 }
302 }
303}
304
305impl SipMessageType {
306 pub fn summary(&self) -> Cow<'_, str> {
308 match self {
309 SipMessageType::Request { method, .. } => Cow::Borrowed(method),
310 SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
311 }
312 }
313}
314
315#[derive(Debug, Clone)]
322pub struct ParsedSipMessage {
323 pub direction: Direction,
325 pub transport: Transport,
327 pub address: String,
329 pub timestamp: Timestamp,
331 pub message_type: SipMessageType,
333 pub headers: Vec<(String, String)>,
336 pub body: Vec<u8>,
338 pub frame_count: usize,
340}
341
342#[derive(Debug, Clone)]
344pub struct MimePart {
345 pub headers: Vec<(String, String)>,
347 pub body: Vec<u8>,
349}
350
351impl MimePart {
352 pub fn content_type(&self) -> Option<&str> {
354 self.headers
355 .iter()
356 .find(|(k, _)| k.eq_ignore_ascii_case("Content-Type"))
357 .map(|(_, v)| v.as_str())
358 }
359
360 fn header_value(&self, name: &str) -> Option<&str> {
361 let name_lower = name.to_ascii_lowercase();
362 self.headers
363 .iter()
364 .find(|(k, _)| k.to_ascii_lowercase() == name_lower)
365 .map(|(_, v)| v.as_str())
366 }
367
368 pub fn content_id(&self) -> Option<&str> {
370 self.header_value("Content-ID")
371 }
372
373 pub fn content_disposition(&self) -> Option<&str> {
375 self.header_value("Content-Disposition")
376 }
377}
378
379impl ParsedSipMessage {
380 pub fn call_id(&self) -> Option<&str> {
383 self.header_value("Call-ID")
384 .or_else(|| self.header_value("i"))
385 }
386
387 pub fn content_type(&self) -> Option<&str> {
390 self.header_value("Content-Type")
391 .or_else(|| self.header_value("c"))
392 }
393
394 pub fn content_length(&self) -> Option<usize> {
397 self.header_value("Content-Length")
398 .or_else(|| self.header_value("l"))
399 .and_then(|v| v.trim().parse().ok())
400 }
401
402 pub fn cseq(&self) -> Option<&str> {
404 self.header_value("CSeq")
405 }
406
407 pub fn method(&self) -> Option<&str> {
410 match &self.message_type {
411 SipMessageType::Request { method, .. } => Some(method),
412 SipMessageType::Response { .. } => {
413 self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
414 }
415 }
416 }
417
418 pub fn body_data(&self) -> Cow<'_, str> {
421 String::from_utf8_lossy(&self.body)
422 }
423
424 pub fn to_bytes(&self) -> Vec<u8> {
426 let mut out = Vec::new();
427 match &self.message_type {
428 SipMessageType::Request { method, uri } => {
429 out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
430 }
431 SipMessageType::Response { code, reason } => {
432 out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
433 }
434 }
435 for (name, value) in &self.headers {
436 out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
437 }
438 out.extend_from_slice(b"\r\n");
439 if !self.body.is_empty() {
440 out.extend_from_slice(&self.body);
441 }
442 out
443 }
444
445 fn header_value(&self, name: &str) -> Option<&str> {
446 let name_lower = name.to_ascii_lowercase();
447 self.headers
448 .iter()
449 .find(|(k, _)| k.to_ascii_lowercase() == name_lower)
450 .map(|(_, v)| v.as_str())
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn make_parsed(
459 msg_type: SipMessageType,
460 headers: Vec<(&str, &str)>,
461 body: &[u8],
462 ) -> ParsedSipMessage {
463 ParsedSipMessage {
464 direction: Direction::Recv,
465 transport: Transport::Tcp,
466 address: "10.0.0.1:5060".into(),
467 timestamp: Timestamp::TimeOnly {
468 hour: 12,
469 min: 0,
470 sec: 0,
471 usec: 0,
472 },
473 message_type: msg_type,
474 headers: headers
475 .iter()
476 .map(|(k, v)| (k.to_string(), v.to_string()))
477 .collect(),
478 body: body.to_vec(),
479 frame_count: 1,
480 }
481 }
482
483 #[test]
484 fn to_bytes_request_no_body() {
485 let msg = make_parsed(
486 SipMessageType::Request {
487 method: "OPTIONS".into(),
488 uri: "sip:host".into(),
489 },
490 vec![("Call-ID", "test")],
491 b"",
492 );
493 let bytes = msg.to_bytes();
494 let text = String::from_utf8(bytes).unwrap();
495 assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
496 assert!(text.contains("Call-ID: test\r\n"));
497 assert!(text.ends_with("\r\n\r\n"));
498 }
499
500 #[test]
501 fn to_bytes_request_with_body() {
502 let body = b"v=0\r\ns=-\r\n";
503 let msg = make_parsed(
504 SipMessageType::Request {
505 method: "INVITE".into(),
506 uri: "sip:host".into(),
507 },
508 vec![("Call-ID", "test")],
509 body,
510 );
511 let bytes = msg.to_bytes();
512 assert!(bytes.ends_with(body));
513 }
514
515 #[test]
516 fn to_bytes_response() {
517 let msg = make_parsed(
518 SipMessageType::Response {
519 code: 200,
520 reason: "OK".into(),
521 },
522 vec![("Call-ID", "resp-test")],
523 b"",
524 );
525 let bytes = msg.to_bytes();
526 let text = String::from_utf8(bytes).unwrap();
527 assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
528 }
529
530 #[test]
531 fn body_data_valid_utf8() {
532 let msg = make_parsed(
533 SipMessageType::Request {
534 method: "MESSAGE".into(),
535 uri: "sip:host".into(),
536 },
537 vec![],
538 b"hello world",
539 );
540 assert_eq!(&*msg.body_data(), "hello world");
541 }
542
543 #[test]
544 fn body_data_empty() {
545 let msg = make_parsed(
546 SipMessageType::Request {
547 method: "OPTIONS".into(),
548 uri: "sip:host".into(),
549 },
550 vec![],
551 b"",
552 );
553 assert_eq!(&*msg.body_data(), "");
554 }
555
556 #[test]
557 fn body_data_binary() {
558 let msg = make_parsed(
559 SipMessageType::Request {
560 method: "MESSAGE".into(),
561 uri: "sip:host".into(),
562 },
563 vec![],
564 &[0xFF, 0xFE],
565 );
566 assert!(msg.body_data().contains('\u{FFFD}'));
567 }
568
569 #[test]
570 fn body_text_non_json_passthrough() {
571 let msg = make_parsed(
572 SipMessageType::Request {
573 method: "INVITE".into(),
574 uri: "sip:host".into(),
575 },
576 vec![("Content-Type", "application/sdp")],
577 b"v=0\r\ns=-\r\n",
578 );
579 assert_eq!(msg.body_text().as_ref(), msg.body_data().as_ref());
580 }
581
582 #[test]
583 fn body_text_json_unescapes_newlines() {
584 let msg = make_parsed(
585 SipMessageType::Request {
586 method: "NOTIFY".into(),
587 uri: "sip:host".into(),
588 },
589 vec![("Content-Type", "application/json")],
590 br#"{"invite":"INVITE sip:host SIP/2.0\r\nTo: <sip:host>\r\n"}"#,
591 );
592 let text = msg.body_text();
593 assert!(
594 text.contains("INVITE sip:host SIP/2.0\r\nTo: <sip:host>\r\n"),
595 "JSON \\r\\n should be unescaped to actual CRLF, got: {text:?}"
596 );
597 }
598
599 #[test]
600 fn body_text_plus_json_content_type() {
601 let msg = make_parsed(
602 SipMessageType::Request {
603 method: "NOTIFY".into(),
604 uri: "sip:host".into(),
605 },
606 vec![(
607 "Content-Type",
608 "application/emergencyCallData.AbandonedCall+json",
609 )],
610 br#"{"invite":"line1\nline2"}"#,
611 );
612 let text = msg.body_text();
613 assert!(
614 text.contains("line1\nline2"),
615 "application/*+json should trigger unescaping, got: {text:?}"
616 );
617 }
618
619 #[test]
620 fn body_data_preserves_json_escapes() {
621 let raw = br#"{"key":"value\nwith\\escapes"}"#;
622 let msg = make_parsed(
623 SipMessageType::Request {
624 method: "NOTIFY".into(),
625 uri: "sip:host".into(),
626 },
627 vec![("Content-Type", "application/json")],
628 raw,
629 );
630 assert_eq!(
631 msg.body_data().as_ref(),
632 r#"{"key":"value\nwith\\escapes"}"#,
633 "body_data() must preserve raw escapes"
634 );
635 }
636}