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}