1use std::borrow::Cow;
2
3use sip_header::extract_all_headers;
4
5use crate::finders::CRLF;
6use crate::frame::ParseError;
7use crate::message::MessageIterator;
8use crate::sip::content_type::{extract_boundary, normalize_media_type};
9use crate::sip::json::unescape_json_body;
10use crate::sip::multipart::{is_multipart_type, split_multipart};
11use crate::startline::{bytes_to_str, parse_first_line, parse_first_line_ref, StartLineRef};
12use crate::types::{
13 value_or_compact, Headers, MimePart, ParseStats, ParsedSipMessage, SipFragment, SipMessage,
14 SkipTracking, UnparsedRegion,
15};
16
17pub(crate) mod content_type;
18mod fragment;
19mod json;
20mod multipart;
21#[cfg(test)]
22pub(crate) mod test_support;
23
24pub use content_type::is_json_content_type;
25pub use fragment::parse_sipfrag;
26
27pub(crate) trait HasHeaders {
31 fn headers(&self) -> &Headers;
32
33 fn body(&self) -> &[u8];
34
35 fn content_type(&self) -> Option<&str> {
36 value_or_compact(self.headers(), "Content-Type")
37 }
38
39 fn media_type(&self) -> Option<Cow<'_, str>> {
40 self.content_type().map(normalize_media_type)
41 }
42
43 fn is_multipart(&self) -> bool {
44 is_multipart_type(self.content_type())
45 }
46
47 fn multipart_boundary(&self) -> Option<&str> {
48 extract_boundary(self.content_type()?)
49 }
50
51 fn body_parts(&self) -> Option<Vec<MimePart>> {
52 split_multipart(self.content_type(), self.body())
53 }
54
55 fn body_text(&self) -> Cow<'_, str> {
56 match self.content_type() {
57 Some(ct) if is_json_content_type(ct) => Cow::Owned(unescape_json_body(self.body())),
58 _ => String::from_utf8_lossy(self.body()),
59 }
60 }
61
62 fn json_field(&self, key: &str) -> Option<String> {
63 let ct = self.content_type()?;
64 if !is_json_content_type(ct) {
65 return None;
66 }
67 let value: serde_json::Value = serde_json::from_slice(self.body()).ok()?;
68 let obj = value.as_object()?;
69 obj.get(key)?.as_str().map(|s| s.to_string())
70 }
71}
72
73impl HasHeaders for ParsedSipMessage {
74 fn headers(&self) -> &Headers {
75 &self.headers
76 }
77
78 fn body(&self) -> &[u8] {
79 &self.body
80 }
81}
82
83impl HasHeaders for MimePart {
84 fn headers(&self) -> &Headers {
85 &self.headers
86 }
87
88 fn body(&self) -> &[u8] {
89 &self.body
90 }
91}
92
93impl HasHeaders for SipFragment {
94 fn headers(&self) -> &Headers {
95 &self.headers
96 }
97
98 fn body(&self) -> &[u8] {
99 &self.body
100 }
101}
102
103impl SipMessage {
104 pub fn parse(&self) -> Result<ParsedSipMessage, ParseError> {
107 parse_sip_message(self)
108 }
109
110 pub fn method(&self) -> Option<&str> {
119 let first_line_end = CRLF.find(&self.content)?;
120 match parse_first_line_ref(&self.content[..first_line_end]).ok()? {
121 StartLineRef::Request { method, .. } => std::str::from_utf8(method).ok(),
122 StartLineRef::Response { .. } => {
123 let (headers, _) = split_headers_body(&self.content, first_line_end + 2);
124 cseq_method(headers)
125 }
126 }
127 }
128}
129
130fn cseq_method(headers: &[u8]) -> Option<&str> {
135 let mut lines = headers.split(|&b| b == b'\n').peekable();
136
137 while let Some(line) = lines.next() {
138 let line = match line {
139 [rest @ .., b'\r'] => rest,
140 rest => rest,
141 };
142 if line.is_empty() {
143 return None;
144 }
145 if matches!(line.first(), Some(b' ' | b'\t')) {
146 continue;
147 }
148 let Some(colon) = memchr::memchr(b':', line) else {
149 continue;
150 };
151 let mut name = &line[..colon];
152 while let [rest @ .., b' ' | b'\t'] = name {
153 name = rest;
154 }
155 if name.contains(&b' ') || !name.eq_ignore_ascii_case(b"CSeq") {
156 continue;
157 }
158
159 if matches!(lines.peek(), Some([b' ' | b'\t', ..])) {
160 return None;
161 }
162 let value = &line[colon + 1..];
163 if !value.is_ascii() {
164 return None;
165 }
166 return std::str::from_utf8(value)
167 .ok()?
168 .split_ascii_whitespace()
169 .nth(1);
170 }
171
172 None
173}
174
175pub struct ParsedMessageIterator<R> {
196 inner: MessageIterator<R>,
197}
198
199impl<R: std::io::Read> ParsedMessageIterator<R> {
200 pub fn new(reader: R) -> Self {
202 ParsedMessageIterator {
203 inner: MessageIterator::new(reader),
204 }
205 }
206
207 pub fn capture_skipped(mut self, enable: bool) -> Self {
211 self.inner = self.inner.capture_skipped(enable);
212 self
213 }
214
215 pub fn skip_tracking(mut self, tracking: SkipTracking) -> Self {
217 self.inner = self.inner.skip_tracking(tracking);
218 self
219 }
220
221 pub fn parse_stats(&self) -> &ParseStats {
223 self.inner.parse_stats()
224 }
225
226 pub fn drain_unparsed(&mut self) -> Vec<UnparsedRegion> {
228 self.inner.drain_unparsed()
229 }
230}
231
232impl<R: std::io::Read> Iterator for ParsedMessageIterator<R> {
233 type Item = Result<ParsedSipMessage, ParseError>;
234
235 fn next(&mut self) -> Option<Self::Item> {
236 let msg = match self.inner.next()? {
237 Ok(m) => m,
238 Err(e) => return Some(Err(e)),
239 };
240 Some(msg.parse())
241 }
242}
243
244fn content_preview(content: &[u8], max_len: usize) -> String {
245 let len = content.len().min(max_len);
246 let s = String::from_utf8_lossy(&content[..len]);
247 let mut out = String::with_capacity(s.len());
248 for c in s.chars() {
249 match c {
250 '\r' => out.push_str("\\r"),
251 '\n' => out.push_str("\\n"),
252 '\t' => out.push_str("\\t"),
253 '\0' => out.push_str("\\0"),
254 c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
255 c => out.push(c),
256 }
257 }
258 if content.len() > max_len {
259 out.push_str("...");
260 }
261 out
262}
263
264fn parse_sip_message(msg: &SipMessage) -> Result<ParsedSipMessage, ParseError> {
265 let content = &msg.content;
266
267 if content
268 .iter()
269 .all(|&b| matches!(b, b'\r' | b'\n' | b' ' | b'\t'))
270 {
271 return Err(ParseError::TransportNoise {
272 bytes: content.len(),
273 transport: msg.transport,
274 address: msg.address.clone(),
275 });
276 }
277
278 parse_sip_content(msg, content).map_err(|e| {
279 let reason = match e {
280 ParseError::InvalidMessage(reason) => reason,
281 other => return other,
282 };
283 let preview = content_preview(content, 200);
284 ParseError::InvalidMessage(format!(
285 "{} {}/{} at {} ({} frames, {} bytes): {reason}\n {preview}",
286 msg.direction,
287 msg.transport,
288 msg.address,
289 msg.timestamp,
290 msg.frame_count,
291 content.len(),
292 ))
293 })
294}
295
296fn parse_sip_content(msg: &SipMessage, content: &[u8]) -> Result<ParsedSipMessage, ParseError> {
297 let first_line_end = CRLF
299 .find(content)
300 .ok_or_else(|| ParseError::InvalidMessage("no CRLF found".into()))?;
301 let first_line = &content[..first_line_end];
302
303 let message_type = parse_first_line(first_line)?;
304
305 let (header_bytes, body) = split_headers_body(content, first_line_end + 2);
306 let headers = parse_headers(header_bytes);
307
308 Ok(ParsedSipMessage {
309 direction: msg.direction,
310 transport: msg.transport,
311 address: msg.address.clone(),
312 timestamp: msg.timestamp,
313 offset: msg.offset,
314 message_type,
315 headers,
316 body: body.to_vec(),
317 frame_count: msg.frame_count,
318 })
319}
320
321pub(crate) fn parse_headers(data: &[u8]) -> Headers {
322 Headers::from(extract_all_headers(&bytes_to_str(data)))
323}
324
325pub(crate) fn split_headers_body(data: &[u8], headers_start: usize) -> (&[u8], &[u8]) {
329 let start = headers_start.min(data.len());
330 let mut pos = start;
331 while let Some(rel) = memchr::memchr(b'\n', &data[pos..]) {
332 let line = match &data[pos..pos + rel] {
333 [rest @ .., b'\r'] => rest,
334 rest => rest,
335 };
336 if line.is_empty() {
337 return (&data[start..pos], &data[pos + rel + 1..]);
338 }
339 pos += rel + 1;
340 }
341 (&data[start..], &[][..])
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::sip::test_support::make_sip_message;
348 use crate::types::{Direction, SipMessage, SipMessageType, Timestamp, Transport};
349
350 fn assert_agrees_with_parse(content: &[u8]) {
353 let msg = make_sip_message(content);
354 if let Some(cheap) = msg.method() {
355 let parsed = msg.parse().expect("classified message must parse");
356 assert_eq!(Some(cheap), parsed.method());
357 }
358 }
359
360 #[test]
361 fn method_from_request_line() {
362 let msg = make_sip_message(b"INVITE sip:user@host SIP/2.0\r\nCSeq: 1 INVITE\r\n\r\n");
363 assert_eq!(msg.method(), Some("INVITE"));
364 }
365
366 #[test]
367 fn method_from_response_cseq() {
368 let msg = make_sip_message(b"SIP/2.0 200 OK\r\nVia: x\r\nCSeq: 42 OPTIONS\r\n\r\n");
369 assert_eq!(msg.method(), Some("OPTIONS"));
370 }
371
372 #[test]
373 fn method_from_response_cseq_name_variants() {
374 let lower = make_sip_message(b"SIP/2.0 200 OK\r\ncseq: 1 BYE\r\n\r\n");
375 assert_eq!(lower.method(), Some("BYE"));
376
377 let padded = make_sip_message(b"SIP/2.0 200 OK\r\nCSeq \t: 1 BYE\r\n\r\n");
378 assert_eq!(padded.method(), Some("BYE"));
379
380 let tabbed = make_sip_message(b"SIP/2.0 200 OK\r\nCSeq:\t1\tBYE\r\n\r\n");
381 assert_eq!(tabbed.method(), Some("BYE"));
382 }
383
384 #[test]
385 fn method_takes_first_cseq_in_wire_order() {
386 let msg = make_sip_message(b"SIP/2.0 200 OK\r\nCSeq: 1 BYE\r\nCSeq: 2 INVITE\r\n\r\n");
387 assert_eq!(msg.method(), Some("BYE"));
388 assert_agrees_with_parse(b"SIP/2.0 200 OK\r\nCSeq: 1 BYE\r\nCSeq: 2 INVITE\r\n\r\n");
389 }
390
391 #[test]
392 fn method_none_on_folded_cseq() {
393 let content = b"SIP/2.0 200 OK\r\nCSeq: 1\r\n INVITE\r\n\r\n";
394 assert_eq!(make_sip_message(content).method(), None);
395 assert_agrees_with_parse(content);
396 }
397
398 #[test]
399 fn method_none_without_cseq() {
400 let msg = make_sip_message(b"SIP/2.0 200 OK\r\nVia: x\r\n\r\nCSeq: 1 INVITE\r\n");
401 assert_eq!(msg.method(), None);
402 }
403
404 #[test]
405 fn method_none_on_malformed_start_line() {
406 assert_eq!(
407 make_sip_message(b"INVITE sip:user@host SIP/3.0\r\nCSeq: 1 INVITE\r\n\r\n").method(),
408 None
409 );
410 assert_eq!(
411 make_sip_message(b"garbage\r\nCSeq: 1 INVITE\r\n\r\n").method(),
412 None
413 );
414 assert_eq!(make_sip_message(b"\r\n\r\n").method(), None);
415 }
416
417 #[test]
420 fn method_none_when_cseq_follows_lf_blank_line() {
421 let content = b"SIP/2.0 200 OK\r\nVia: x\n\r\nCSeq: 1 OPTIONS\r\n\r\n";
422 let parsed = make_sip_message(content).parse().unwrap();
423 assert_eq!(
424 parsed.method(),
425 None,
426 "precondition: the parsed side cannot see this CSeq"
427 );
428 assert_eq!(make_sip_message(content).method(), None);
429
430 assert_eq!(parsed.headers.len(), 1);
431 assert_eq!(parsed.header_value("Via"), Some("x"));
432 assert_eq!(parsed.body, b"CSeq: 1 OPTIONS\r\n\r\n");
433
434 let start_line = b"SIP/2.0 200 OK\r\n".len();
435 let (headers, body) = split_headers_body(content, start_line);
436 assert_eq!(headers, b"Via: x\n");
437 assert_eq!(
438 start_line + headers.len() + b"\r\n".len() + body.len(),
439 content.len(),
440 "every byte lands in the start line, the headers, the blank line or the body"
441 );
442 }
443
444 #[test]
446 fn method_none_on_non_ascii_cseq_value() {
447 let content = "SIP/2.0 200 OK\r\nCSeq: 1\u{a0}2 OPTIONS\r\n\r\n".as_bytes();
448 assert_eq!(make_sip_message(content).method(), None);
449 assert_agrees_with_parse(content);
450 }
451
452 #[test]
453 fn method_none_on_transport_noise() {
454 assert_eq!(make_sip_message(b"\r\n\r\n\r\n").method(), None);
455 assert_eq!(make_sip_message(b"").method(), None);
456 }
457
458 #[test]
459 fn non_utf8_header_value_falls_back_to_lossy() {
460 let mut content = b"OPTIONS sip:host SIP/2.0\r\nSubject: caf".to_vec();
461 content.push(0xE9);
462 content.extend_from_slice(b"\r\nContent-Length: 0\r\n\r\n");
463
464 let parsed = make_sip_message(&content).parse().unwrap();
465 assert_eq!(parsed.header_value("Subject"), Some("caf\u{fffd}"));
466 }
467
468 #[test]
469 fn parse_stats_delegates() {
470 let content =
471 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: stats-test\r\nContent-Length: 0\r\n\r\n";
472 let header = format!(
473 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
474 content.len()
475 );
476 let mut data = header.into_bytes();
477 data.extend_from_slice(content);
478 data.extend_from_slice(b"\x0B\n");
479
480 let mut iter = ParsedMessageIterator::new(&data[..]);
481 let parsed: Vec<_> = iter.by_ref().collect::<Result<Vec<_>, _>>().unwrap();
482 assert_eq!(parsed.len(), 1);
483 let stats = iter.parse_stats();
484 assert_eq!(stats.bytes_read, data.len() as u64);
485 assert_eq!(stats.bytes_skipped, 0);
486 }
487
488 #[test]
489 fn parse_options_request() {
490 let content = b"OPTIONS sip:user@host SIP/2.0\r\n\
491 Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-1\r\n\
492 From: <sip:user@host>;tag=abc\r\n\
493 To: <sip:user@host>\r\n\
494 Call-ID: test-call-id@host\r\n\
495 CSeq: 1 OPTIONS\r\n\
496 Content-Length: 0\r\n\
497 \r\n";
498 let msg = make_sip_message(content);
499 let parsed = msg.parse().unwrap();
500
501 assert_eq!(
502 parsed.message_type,
503 SipMessageType::Request {
504 method: "OPTIONS".into(),
505 uri: "sip:user@host".into()
506 }
507 );
508 assert_eq!(parsed.call_id(), Some("test-call-id@host"));
509 assert_eq!(parsed.cseq(), Some("1 OPTIONS"));
510 assert_eq!(parsed.content_length(), Some(0));
511 assert_eq!(parsed.method(), Some("OPTIONS"));
512 assert!(parsed.body.is_empty());
513 }
514
515 #[test]
516 fn parse_200_ok_response() {
517 let content = b"SIP/2.0 200 OK\r\n\
518 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
519 Call-ID: resp-id@host\r\n\
520 CSeq: 1 INVITE\r\n\
521 Content-Length: 0\r\n\
522 \r\n";
523 let msg = make_sip_message(content);
524 let parsed = msg.parse().unwrap();
525
526 assert_eq!(
527 parsed.message_type,
528 SipMessageType::Response {
529 code: 200,
530 reason: "OK".into()
531 }
532 );
533 assert_eq!(parsed.method(), Some("INVITE"));
534 }
535
536 #[test]
537 fn parse_100_trying() {
538 let content = b"SIP/2.0 100 Trying\r\n\
539 Via: SIP/2.0/TCP 10.0.0.1:5060\r\n\
540 Call-ID: trying-id\r\n\
541 CSeq: 42 INVITE\r\n\
542 Content-Length: 0\r\n\
543 \r\n";
544 let msg = make_sip_message(content);
545 let parsed = msg.parse().unwrap();
546
547 assert_eq!(
548 parsed.message_type,
549 SipMessageType::Response {
550 code: 100,
551 reason: "Trying".into()
552 }
553 );
554 assert_eq!(parsed.method(), Some("INVITE"));
555 }
556
557 #[test]
558 fn parse_invite_with_sdp_body() {
559 let body = b"v=0\r\no=- 123 456 IN IP4 10.0.0.1\r\ns=-\r\n";
560 let mut content = Vec::new();
561 content.extend_from_slice(b"INVITE sip:user@host SIP/2.0\r\n");
562 content.extend_from_slice(b"Call-ID: invite-body@host\r\n");
563 content.extend_from_slice(b"CSeq: 1 INVITE\r\n");
564 content.extend_from_slice(b"Content-Type: application/sdp\r\n");
565 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
566 content.extend_from_slice(b"\r\n");
567 content.extend_from_slice(body);
568
569 let msg = make_sip_message(&content);
570 let parsed = msg.parse().unwrap();
571
572 assert_eq!(parsed.method(), Some("INVITE"));
573 assert_eq!(parsed.content_type(), Some("application/sdp"));
574 assert_eq!(parsed.content_length(), Some(body.len()));
575 assert_eq!(parsed.body, body);
576 }
577
578 #[test]
579 fn parse_notify_with_json_body() {
580 let body = br#"{"event":"AbandonedCall","id":"123"}"#;
581 let mut content = Vec::new();
582 content.extend_from_slice(b"NOTIFY sip:user@host SIP/2.0\r\n");
583 content.extend_from_slice(b"Call-ID: notify-json@host\r\n");
584 content.extend_from_slice(b"CSeq: 1 NOTIFY\r\n");
585 content.extend_from_slice(b"Content-Type: application/json\r\n");
586 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
587 content.extend_from_slice(b"\r\n");
588 content.extend_from_slice(body);
589
590 let msg = make_sip_message(&content);
591 let parsed = msg.parse().unwrap();
592
593 assert_eq!(parsed.method(), Some("NOTIFY"));
594 assert_eq!(parsed.content_type(), Some("application/json"));
595 assert_eq!(parsed.body, body);
596 }
597
598 #[test]
599 fn compact_headers() {
600 let content = b"NOTIFY sip:user@host SIP/2.0\r\n\
601 i: compact-call-id\r\n\
602 l: 0\r\n\
603 c: text/plain\r\n\
604 \r\n";
605 let msg = make_sip_message(content);
606 let parsed = msg.parse().unwrap();
607
608 assert_eq!(parsed.call_id(), Some("compact-call-id"));
609 assert_eq!(parsed.content_length(), Some(0));
610 assert_eq!(parsed.content_type(), Some("text/plain"));
611 }
612
613 #[test]
614 fn header_folding() {
615 let content = b"OPTIONS sip:host SIP/2.0\r\n\
616 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
617 Subject: this is a long\r\n \
618 folded header value\r\n\
619 Call-ID: fold-test\r\n\
620 Content-Length: 0\r\n\
621 \r\n";
622 let msg = make_sip_message(content);
623 let parsed = msg.parse().unwrap();
624
625 let subject = parsed
626 .headers
627 .iter()
628 .find(|(k, _)| k == "Subject")
629 .map(|(_, v)| v.as_str());
630 assert_eq!(subject, Some("this is a long folded header value"));
631 assert_eq!(parsed.call_id(), Some("fold-test"));
632 }
633
634 #[test]
635 fn folded_header_no_crlf_leak() {
636 let content = b"OPTIONS sip:host SIP/2.0\r\n\
637 Subject: line1\r\n \
638 line2\r\n\
639 Content-Length: 0\r\n\
640 \r\n";
641 let msg = make_sip_message(content);
642 let parsed = msg.parse().unwrap();
643 let subject = parsed.headers.iter().find(|(k, _)| k == "Subject").unwrap();
644 assert!(!subject.1.contains('\r'), "CRLF leaked: {:?}", subject.1);
645 assert!(!subject.1.contains('\n'), "LF leaked: {:?}", subject.1);
646 assert_eq!(subject.1, "line1 line2");
647 }
648
649 #[test]
650 fn no_body() {
651 let content = b"OPTIONS sip:host SIP/2.0\r\n\
652 Call-ID: nobody\r\n\
653 Content-Length: 0\r\n\
654 \r\n";
655 let msg = make_sip_message(content);
656 let parsed = msg.parse().unwrap();
657 assert!(parsed.body.is_empty());
658 }
659
660 #[test]
661 fn no_blank_line_no_body() {
662 let content = b"OPTIONS sip:host SIP/2.0\r\n\
664 Call-ID: no-blank\r\n\
665 Content-Length: 0";
666 let msg = make_sip_message(content);
667 let parsed = msg.parse().unwrap();
668 assert!(parsed.body.is_empty());
669 assert_eq!(parsed.call_id(), Some("no-blank"));
670 }
671
672 #[test]
673 fn preserves_metadata() {
674 let content = b"REGISTER sip:host SIP/2.0\r\n\
675 Call-ID: meta-test\r\n\
676 \r\n";
677 let msg = SipMessage {
678 direction: Direction::Sent,
679 transport: Transport::Tls,
680 address: "[2001:db8::1]:5061".into(),
681 timestamp: Timestamp::DateTime {
682 year: 2026,
683 month: 2,
684 day: 12,
685 hour: 10,
686 min: 30,
687 sec: 0,
688 usec: 123456,
689 },
690 content: content.to_vec(),
691 offset: 0,
692 frame_count: 3,
693 };
694 let parsed = msg.parse().unwrap();
695
696 assert_eq!(parsed.direction, Direction::Sent);
697 assert_eq!(parsed.transport, Transport::Tls);
698 assert_eq!(parsed.address, "[2001:db8::1]:5061");
699 assert_eq!(parsed.frame_count, 3);
700 assert_eq!(
701 parsed.timestamp,
702 Timestamp::DateTime {
703 year: 2026,
704 month: 2,
705 day: 12,
706 hour: 10,
707 min: 30,
708 sec: 0,
709 usec: 123456,
710 }
711 );
712 }
713
714 #[test]
715 fn multiple_same_name_headers() {
716 let content = b"INVITE sip:host SIP/2.0\r\n\
717 Via: SIP/2.0/UDP proxy1:5060\r\n\
718 Via: SIP/2.0/UDP proxy2:5060\r\n\
719 Record-Route: <sip:proxy1>\r\n\
720 Record-Route: <sip:proxy2>\r\n\
721 Call-ID: multi-hdr\r\n\
722 Content-Length: 0\r\n\
723 \r\n";
724 let msg = make_sip_message(content);
725 let parsed = msg.parse().unwrap();
726
727 let via_count = parsed.headers.iter().filter(|(k, _)| k == "Via").count();
728 assert_eq!(via_count, 2);
729
730 let rr_count = parsed
731 .headers
732 .iter()
733 .filter(|(k, _)| k == "Record-Route")
734 .count();
735 assert_eq!(rr_count, 2);
736 }
737
738 #[test]
739 fn header_ordering_preserved() {
740 let content = b"OPTIONS sip:host SIP/2.0\r\n\
741 Via: v1\r\n\
742 From: f1\r\n\
743 To: t1\r\n\
744 Call-ID: order-test\r\n\
745 CSeq: 1 OPTIONS\r\n\
746 \r\n";
747 let msg = make_sip_message(content);
748 let parsed = msg.parse().unwrap();
749
750 let names: Vec<&str> = parsed.headers.iter().map(|(k, _)| k.as_str()).collect();
751 assert_eq!(names, vec!["Via", "From", "To", "Call-ID", "CSeq"]);
752 }
753
754 #[test]
755 fn binary_body() {
756 let body: Vec<u8> = (0..256).map(|i| i as u8).collect();
757 let mut content = Vec::new();
758 content.extend_from_slice(b"MESSAGE sip:host SIP/2.0\r\n");
759 content.extend_from_slice(b"Call-ID: binary-body\r\n");
760 content.extend_from_slice(b"Content-Type: application/octet-stream\r\n");
761 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
762 content.extend_from_slice(b"\r\n");
763 content.extend_from_slice(&body);
764
765 let msg = make_sip_message(&content);
766 let parsed = msg.parse().unwrap();
767
768 assert_eq!(parsed.body, body);
769 }
770
771 #[test]
772 fn error_no_crlf() {
773 let content = b"garbage without any crlf";
774 let msg = make_sip_message(content);
775 let result = msg.parse();
776 assert!(result.is_err());
777 }
778
779 #[test]
780 fn header_value_with_colon() {
781 let content = b"INVITE sip:host SIP/2.0\r\n\
783 Contact: <sip:user@10.0.0.1:5060;transport=tcp>\r\n\
784 Call-ID: colon-val\r\n\
785 \r\n";
786 let msg = make_sip_message(content);
787 let parsed = msg.parse().unwrap();
788
789 let contact = parsed
790 .headers
791 .iter()
792 .find(|(k, _)| k == "Contact")
793 .map(|(_, v)| v.as_str());
794 assert_eq!(contact, Some("<sip:user@10.0.0.1:5060;transport=tcp>"));
795 }
796
797 #[test]
798 fn whitespace_around_header_value() {
799 let content = b"OPTIONS sip:host SIP/2.0\r\n\
800 Call-ID: spaces-around \r\n\
801 \r\n";
802 let msg = make_sip_message(content);
803 let parsed = msg.parse().unwrap();
804
805 assert_eq!(parsed.call_id(), Some("spaces-around "));
807 }
808
809 #[test]
810 fn parsed_message_iterator() {
811 let content =
812 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: iter-test\r\nContent-Length: 0\r\n\r\n";
813 let header = format!(
814 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
815 content.len()
816 );
817 let mut data = header.into_bytes();
818 data.extend_from_slice(content);
819 data.extend_from_slice(b"\x0B\n");
820
821 let parsed: Vec<ParsedSipMessage> = ParsedMessageIterator::new(&data[..])
822 .collect::<Result<Vec<_>, _>>()
823 .unwrap();
824
825 assert_eq!(parsed.len(), 1);
826 assert_eq!(parsed[0].call_id(), Some("iter-test"));
827 assert_eq!(parsed[0].method(), Some("OPTIONS"));
828 }
829
830 #[test]
831 fn whitespace_only_returns_transport_noise() {
832 use crate::frame::ParseError;
833
834 for content in [b"\n".as_slice(), b"\r\n", b"\n\n\n", b" \t\r\n"] {
835 let msg = SipMessage {
836 direction: Direction::Recv,
837 transport: Transport::Tls,
838 address: "[10.0.0.1]:5061".into(),
839 timestamp: Timestamp::TimeOnly {
840 hour: 0,
841 min: 0,
842 sec: 0,
843 usec: 0,
844 },
845 content: content.to_vec(),
846 offset: 0,
847 frame_count: 1,
848 };
849 let err = msg.parse().unwrap_err();
850 assert!(
851 matches!(err, ParseError::TransportNoise { .. }),
852 "whitespace-only content {:?} should produce TransportNoise, got: {err}",
853 content,
854 );
855 }
856 }
857}