1use std::borrow::Cow;
2use std::sync::LazyLock;
3
4use memchr::memmem;
5use sip_header::extract_all_headers;
6
7use crate::frame::ParseError;
8use crate::message::MessageIterator;
9use crate::types::{
10 Headers, MimePart, ParseStats, ParsedSipMessage, SipFragment, SipMessage, SipMessageType,
11 SkipTracking, UnparsedRegion,
12};
13
14static CRLF: LazyLock<memmem::Finder<'static>> = LazyLock::new(|| memmem::Finder::new(b"\r\n"));
15static CRLFCRLF: LazyLock<memmem::Finder<'static>> =
16 LazyLock::new(|| memmem::Finder::new(b"\r\n\r\n"));
17
18impl SipMessage {
19 pub fn parse(&self) -> Result<ParsedSipMessage, ParseError> {
22 parse_sip_message(self)
23 }
24}
25
26pub struct ParsedMessageIterator<R> {
47 inner: MessageIterator<R>,
48}
49
50impl<R: std::io::Read> ParsedMessageIterator<R> {
51 pub fn new(reader: R) -> Self {
53 ParsedMessageIterator {
54 inner: MessageIterator::new(reader),
55 }
56 }
57
58 pub fn capture_skipped(mut self, enable: bool) -> Self {
60 self.inner = self.inner.capture_skipped(enable);
61 self
62 }
63
64 pub fn skip_tracking(mut self, tracking: SkipTracking) -> Self {
66 self.inner = self.inner.skip_tracking(tracking);
67 self
68 }
69
70 pub fn parse_stats(&self) -> &ParseStats {
72 self.inner.parse_stats()
73 }
74
75 pub fn parse_stats_mut(&mut self) -> &mut ParseStats {
77 self.inner.parse_stats_mut()
78 }
79
80 pub fn drain_unparsed(&mut self) -> Vec<UnparsedRegion> {
82 self.inner.drain_unparsed()
83 }
84}
85
86impl<R: std::io::Read> Iterator for ParsedMessageIterator<R> {
87 type Item = Result<ParsedSipMessage, ParseError>;
88
89 fn next(&mut self) -> Option<Self::Item> {
90 let msg = match self.inner.next()? {
91 Ok(m) => m,
92 Err(e) => return Some(Err(e)),
93 };
94 Some(msg.parse())
95 }
96}
97
98fn content_preview(content: &[u8], max_len: usize) -> String {
99 use std::fmt::Write;
100 let len = content.len().min(max_len);
101 let s = String::from_utf8_lossy(&content[..len]);
102 let mut out = String::with_capacity(s.len());
103 for c in s.chars() {
104 match c {
105 '\r' => out.push_str("\\r"),
106 '\n' => out.push_str("\\n"),
107 '\t' => out.push_str("\\t"),
108 '\0' => out.push_str("\\0"),
109 c if c.is_control() => {
110 let _ = write!(out, "\\x{:02x}", c as u32);
111 }
112 c => out.push(c),
113 }
114 }
115 if content.len() > max_len {
116 out.push_str("...");
117 }
118 out
119}
120
121fn parse_sip_message(msg: &SipMessage) -> Result<ParsedSipMessage, ParseError> {
122 let content = &msg.content;
123
124 if content
125 .iter()
126 .all(|&b| matches!(b, b'\r' | b'\n' | b' ' | b'\t'))
127 {
128 return Err(ParseError::TransportNoise {
129 bytes: content.len(),
130 transport: msg.transport,
131 address: msg.address.clone(),
132 });
133 }
134
135 parse_sip_content(msg, content).map_err(|e| {
136 let reason = match e {
137 ParseError::InvalidMessage(reason) => reason,
138 other => return other,
139 };
140 let preview = content_preview(content, 200);
141 ParseError::InvalidMessage(format!(
142 "{} {}/{} at {} ({} frames, {} bytes): {reason}\n {preview}",
143 msg.direction,
144 msg.transport,
145 msg.address,
146 msg.timestamp,
147 msg.frame_count,
148 content.len(),
149 ))
150 })
151}
152
153fn parse_sip_content(msg: &SipMessage, content: &[u8]) -> Result<ParsedSipMessage, ParseError> {
154 let first_line_end = CRLF
156 .find(content)
157 .ok_or_else(|| ParseError::InvalidMessage("no CRLF found".into()))?;
158 let first_line = &content[..first_line_end];
159
160 let message_type = parse_first_line(first_line)?;
161
162 let (header_bytes, body) = split_headers_body(content, first_line_end + 2);
163 let headers = parse_headers(header_bytes);
164
165 Ok(ParsedSipMessage {
166 direction: msg.direction,
167 transport: msg.transport,
168 address: msg.address.clone(),
169 timestamp: msg.timestamp,
170 message_type,
171 headers,
172 body: body.to_vec(),
173 frame_count: msg.frame_count,
174 })
175}
176
177fn parse_first_line(line: &[u8]) -> Result<SipMessageType, ParseError> {
178 if line.starts_with(b"SIP/2.0 ") {
179 return parse_status_line(line);
180 }
181 parse_request_line(line)
182}
183
184fn parse_status_line(line: &[u8]) -> Result<SipMessageType, ParseError> {
185 let after_version = &line[8..]; let space = memchr::memchr(b' ', after_version)
189 .ok_or_else(|| ParseError::InvalidMessage("no space after status code".into()))?;
190 let code_bytes = &after_version[..space];
191 let code: u16 = std::str::from_utf8(code_bytes)
192 .map_err(|_| ParseError::InvalidMessage("non-UTF-8 status code".into()))?
193 .parse()
194 .map_err(|_| ParseError::InvalidMessage("invalid status code".into()))?;
195
196 let reason = &after_version[space + 1..];
197 let reason = bytes_to_string(reason);
198
199 Ok(SipMessageType::Response { code, reason })
200}
201
202fn is_sip_token(b: &[u8]) -> bool {
203 !b.is_empty()
204 && b.iter()
205 .all(|&c| c.is_ascii_alphanumeric() || b"-._!%*+'~".contains(&c))
206}
207
208fn is_header_line(line: &[u8]) -> bool {
211 let Some(colon) = memchr::memchr(b':', line) else {
212 return false;
213 };
214 let mut name = &line[..colon];
215 while let [rest @ .., b' ' | b'\t'] = name {
216 name = rest;
217 }
218 is_sip_token(name)
219}
220
221fn parse_request_line(line: &[u8]) -> Result<SipMessageType, ParseError> {
222 let first_space = memchr::memchr(b' ', line)
224 .ok_or_else(|| ParseError::InvalidMessage("no space in request line".into()))?;
225 let method = &line[..first_space];
226
227 if !is_sip_token(method) {
228 return Err(ParseError::InvalidMessage(format!(
229 "invalid SIP method: {:?}",
230 String::from_utf8_lossy(method)
231 )));
232 }
233 let rest = &line[first_space + 1..];
234
235 let last_space = memchr::memrchr(b' ', rest)
236 .ok_or_else(|| ParseError::InvalidMessage("no SIP version in request line".into()))?;
237 let version = &rest[last_space + 1..];
238 if version != b"SIP/2.0" {
239 return Err(ParseError::InvalidMessage(format!(
240 "expected SIP/2.0, got {:?}",
241 String::from_utf8_lossy(version)
242 )));
243 }
244 let uri = &rest[..last_space];
245
246 let method = bytes_to_string(method);
247 let uri = bytes_to_string(uri);
248
249 Ok(SipMessageType::Request { method, uri })
250}
251
252fn bytes_to_string(b: &[u8]) -> String {
253 if b.is_ascii() {
254 unsafe { String::from_utf8_unchecked(b.to_vec()) }
256 } else {
257 String::from_utf8_lossy(b).into_owned()
258 }
259}
260
261fn parse_headers(data: &[u8]) -> Headers {
262 let text = String::from_utf8_lossy(data);
263 Headers(extract_all_headers(&text))
264}
265
266fn split_headers_body(data: &[u8], headers_start: usize) -> (&[u8], &[u8]) {
270 match CRLFCRLF.find(data) {
271 Some(pos) if pos >= headers_start => (&data[headers_start..pos], &data[pos + 4..]),
272 Some(pos) => (&[][..], &data[pos + 4..]),
273 None => (&data[headers_start.min(data.len())..], &[][..]),
274 }
275}
276
277pub fn parse_sipfrag(data: &[u8]) -> Result<SipFragment, ParseError> {
284 if data.is_empty() {
285 return Err(ParseError::InvalidMessage("empty sipfrag".into()));
286 }
287
288 let first_line_end = CRLF.find(data).unwrap_or(data.len());
289 let mut first_line = &data[..first_line_end];
290 if let [rest @ .., b'\n'] = first_line {
293 first_line = rest;
294 }
295 if let [rest @ .., b'\r'] = first_line {
296 first_line = rest;
297 }
298
299 let (message_type, headers_start) = match parse_first_line(first_line) {
300 Ok(mt) => (Some(mt), (first_line_end + 2).min(data.len())),
301 Err(e) => {
302 if !is_header_line(first_line) {
303 return Err(e);
304 }
305 (None, 0)
306 }
307 };
308
309 let (header_bytes, body) = split_headers_body(data, headers_start);
310
311 Ok(SipFragment {
312 message_type,
313 headers: parse_headers(header_bytes),
314 body: body.to_vec(),
315 })
316}
317
318fn is_multipart_type(content_type: Option<&str>) -> bool {
319 content_type
320 .map(|ct| normalize_media_type(ct).starts_with("multipart/"))
321 .unwrap_or(false)
322}
323
324fn split_multipart(content_type: Option<&str>, body: &[u8]) -> Option<Vec<MimePart>> {
327 let boundary = extract_boundary(content_type?)?;
328 let parts = parse_multipart_body(body, boundary);
329 (!parts.is_empty()).then_some(parts)
330}
331
332impl SipFragment {
333 pub fn media_type(&self) -> Option<Cow<'_, str>> {
337 self.content_type().map(normalize_media_type)
338 }
339}
340
341impl MimePart {
342 pub fn media_type(&self) -> Option<Cow<'_, str>> {
346 self.content_type().map(normalize_media_type)
347 }
348
349 pub fn is_multipart(&self) -> bool {
351 is_multipart_type(self.content_type())
352 }
353
354 pub fn multipart_boundary(&self) -> Option<&str> {
356 extract_boundary(self.content_type()?)
357 }
358
359 pub fn parse_sipfrag(&self) -> Result<SipFragment, ParseError> {
364 parse_sipfrag(&self.body)
365 }
366
367 pub fn body_parts(&self) -> Option<Vec<MimePart>> {
375 split_multipart(self.content_type(), &self.body)
376 }
377}
378
379impl ParsedSipMessage {
380 pub fn media_type(&self) -> Option<Cow<'_, str>> {
384 self.content_type().map(normalize_media_type)
385 }
386
387 pub fn is_multipart(&self) -> bool {
389 is_multipart_type(self.content_type())
390 }
391
392 pub fn multipart_boundary(&self) -> Option<&str> {
394 extract_boundary(self.content_type()?)
395 }
396
397 pub fn body_parts(&self) -> Option<Vec<MimePart>> {
401 split_multipart(self.content_type(), &self.body)
402 }
403
404 pub fn body_as_parts(&self) -> Vec<MimePart> {
422 if self.body.is_empty() {
423 return Vec::new();
424 }
425 if let Some(parts) = self.body_parts() {
426 return parts;
427 }
428
429 let mut headers: Vec<(String, String)> = Vec::new();
430 if let Some(ct) = self.content_type() {
431 headers.push(("Content-Type".to_string(), ct.to_string()));
432 }
433 for (name, value) in &self.headers {
434 let Some(canonical) = canonical_body_header(name) else {
435 continue;
436 };
437 if headers
438 .iter()
439 .any(|(k, _)| k.eq_ignore_ascii_case(canonical))
440 {
441 continue;
442 }
443 headers.push((canonical.to_string(), value.clone()));
444 }
445 vec![MimePart {
446 headers: Headers(headers),
447 body: self.body.clone(),
448 }]
449 }
450
451 pub fn body_text(&self) -> Cow<'_, str> {
456 if let Some(ct) = self.content_type() {
457 if is_json_content_type(ct) {
458 return Cow::Owned(unescape_json_body(&self.body));
459 }
460 }
461 self.body_data()
462 }
463
464 pub fn json_field(&self, key: &str) -> Option<String> {
468 let ct = self.content_type()?;
469 if !is_json_content_type(ct) {
470 return None;
471 }
472 let value: serde_json::Value = serde_json::from_slice(&self.body).ok()?;
473 let obj = value.as_object()?;
474 obj.get(key)?.as_str().map(|s| s.to_string())
475 }
476}
477
478fn normalize_media_type(ct: &str) -> Cow<'_, str> {
481 let base = ct.split(';').next().unwrap_or("").trim();
482 if base.bytes().any(|b| b.is_ascii_uppercase()) {
483 Cow::Owned(base.to_ascii_lowercase())
484 } else {
485 Cow::Borrowed(base)
486 }
487}
488
489fn canonical_body_header(name: &str) -> Option<&str> {
492 if name.eq_ignore_ascii_case("c") {
493 return Some("Content-Type");
494 }
495 if name.eq_ignore_ascii_case("e") {
496 return Some("Content-Encoding");
497 }
498 if name.eq_ignore_ascii_case("l") || name.eq_ignore_ascii_case("Content-Length") {
499 return None;
500 }
501 name.as_bytes()
502 .get(..8)
503 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"content-"))
504 .then_some(name)
505}
506
507pub fn is_json_content_type(ct: &str) -> bool {
510 let media_type = normalize_media_type(ct);
511 media_type == "application/json"
512 || (media_type.starts_with("application/") && media_type.ends_with("+json"))
513}
514
515fn unescape_json_body(input: &[u8]) -> String {
516 let s = String::from_utf8_lossy(input);
517 let mut out = String::with_capacity(s.len());
518 let mut chars = s.chars();
519
520 while let Some(c) = chars.next() {
521 if c != '\\' {
522 out.push(c);
523 continue;
524 }
525 match chars.next() {
526 Some('"') => out.push('"'),
527 Some('\\') => out.push('\\'),
528 Some('/') => out.push('/'),
529 Some('b') => out.push('\x08'),
530 Some('f') => out.push('\x0C'),
531 Some('n') => out.push('\n'),
532 Some('r') => out.push('\r'),
533 Some('t') => out.push('\t'),
534 Some('u') => unescape_unicode(&mut chars, &mut out),
535 Some(other) => {
536 out.push('\\');
537 out.push(other);
538 }
539 None => out.push('\\'),
540 }
541 }
542 out
543}
544
545fn unescape_unicode(chars: &mut std::str::Chars<'_>, out: &mut String) {
546 let hex: String = chars.by_ref().take(4).collect();
547 let Some(code_point) = parse_hex4(&hex) else {
548 out.push_str("\\u");
549 out.push_str(&hex);
550 return;
551 };
552
553 if (0xD800..=0xDBFF).contains(&code_point) {
554 let mut peek = chars.clone();
555 if peek.next() == Some('\\') && peek.next() == Some('u') {
556 let hex2: String = peek.by_ref().take(4).collect();
557 if let Some(low) = parse_hex4(&hex2) {
558 if (0xDC00..=0xDFFF).contains(&low) {
559 let combined =
560 0x10000 + ((code_point as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
561 if let Some(ch) = char::from_u32(combined) {
562 out.push(ch);
563 *chars = peek;
564 return;
565 }
566 }
567 }
568 }
569 out.push_str("\\u");
570 out.push_str(&hex);
571 } else if let Some(ch) = char::from_u32(code_point as u32) {
572 out.push(ch);
573 } else {
574 out.push_str("\\u");
575 out.push_str(&hex);
576 }
577}
578
579fn parse_hex4(hex: &str) -> Option<u16> {
580 if hex.len() == 4 {
581 u16::from_str_radix(hex, 16).ok()
582 } else {
583 None
584 }
585}
586
587fn extract_boundary(content_type: &str) -> Option<&str> {
588 let lower = content_type.to_ascii_lowercase();
589 let idx = lower.find("boundary=")?;
590 let after = &content_type[idx + 9..];
591
592 if let Some(after_quote) = after.strip_prefix('"') {
593 let end_quote = after_quote.find('"')?;
594 Some(&after_quote[..end_quote])
595 } else {
596 let end = after.find(';').unwrap_or(after.len());
597 let boundary = after[..end].trim();
598 if boundary.is_empty() {
599 None
600 } else {
601 Some(boundary)
602 }
603 }
604}
605
606enum BoundaryTail {
609 Open(usize),
612 Close,
613 End,
615}
616
617fn boundary_tail(rest: &[u8]) -> Option<BoundaryTail> {
620 if rest.starts_with(b"--") {
621 return Some(BoundaryTail::Close);
622 }
623 let pad = rest
624 .iter()
625 .position(|&b| b != b' ' && b != b'\t')
626 .unwrap_or(rest.len());
627 match &rest[pad..] {
628 [] => Some(BoundaryTail::End),
629 [b'\r', b'\n', ..] => Some(BoundaryTail::Open(pad + 2)),
630 _ => None,
631 }
632}
633
634fn next_delimiter(
638 body: &[u8],
639 from: usize,
640 dash_boundary: &[u8],
641 anchored: &memmem::Finder<'_>,
642) -> Option<(usize, usize, BoundaryTail)> {
643 if from == 0 && body.starts_with(dash_boundary) {
644 if let Some(tail) = boundary_tail(&body[dash_boundary.len()..]) {
645 return Some((0, dash_boundary.len(), tail));
646 }
647 }
648 let mut search = from;
649 while let Some(rel) = anchored.find(&body[search..]) {
650 let crlf = search + rel;
651 let token_end = crlf + 2 + dash_boundary.len();
652 if let Some(tail) = boundary_tail(&body[token_end..]) {
653 return Some((crlf, token_end, tail));
654 }
655 search = crlf + 1;
656 }
657 None
658}
659
660fn parse_multipart_body(body: &[u8], boundary: &str) -> Vec<MimePart> {
661 let mut pattern = Vec::with_capacity(boundary.len() + 4);
662 pattern.extend_from_slice(b"\r\n--");
663 pattern.extend_from_slice(boundary.as_bytes());
664 let anchored = memmem::Finder::new(&pattern);
665 let dash_boundary = &pattern[2..];
666
667 let mut parts = Vec::new();
668
669 let Some((_, token_end, tail)) = next_delimiter(body, 0, dash_boundary, &anchored) else {
670 return parts;
671 };
672 let mut cursor = match tail {
673 BoundaryTail::Open(skip) => token_end + skip,
674 BoundaryTail::Close | BoundaryTail::End => return parts,
677 };
678
679 loop {
680 match next_delimiter(body, cursor, dash_boundary, &anchored) {
681 Some((part_end, token_end, BoundaryTail::Open(skip))) => {
682 parts.push(parse_mime_part(&body[cursor..part_end]));
683 cursor = token_end + skip;
684 }
685 Some((part_end, _, BoundaryTail::Close | BoundaryTail::End)) => {
686 parts.push(parse_mime_part(&body[cursor..part_end]));
687 break;
688 }
689 None => {
692 parts.push(parse_mime_part(&body[cursor..]));
693 break;
694 }
695 }
696 }
697 parts
698}
699
700fn parse_mime_part(data: &[u8]) -> MimePart {
701 match CRLFCRLF.find(data) {
702 Some(pos) => {
703 let header_bytes = &data[..pos];
704 let body = &data[pos + 4..];
705 let headers = parse_headers(header_bytes);
706 MimePart {
707 headers,
708 body: body.to_vec(),
709 }
710 }
711 None => {
712 let first_line_end = CRLF.find(data).unwrap_or(data.len());
715 if memchr::memchr(b':', &data[..first_line_end]).is_some() {
716 let headers = parse_headers(data);
717 MimePart {
718 headers,
719 body: Vec::new(),
720 }
721 } else {
722 MimePart {
723 headers: Headers::default(),
724 body: data.to_vec(),
725 }
726 }
727 }
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734 use crate::types::{Direction, SipMessage, Timestamp, Transport};
735
736 fn make_sip_message(content: &[u8]) -> SipMessage {
737 SipMessage {
738 direction: Direction::Recv,
739 transport: Transport::Udp,
740 address: "10.0.0.1:5060".into(),
741 timestamp: Timestamp::TimeOnly {
742 hour: 12,
743 min: 0,
744 sec: 0,
745 usec: 0,
746 },
747 content: content.to_vec(),
748 frame_count: 1,
749 }
750 }
751
752 #[test]
753 fn parse_stats_delegates() {
754 let content =
755 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: stats-test\r\nContent-Length: 0\r\n\r\n";
756 let header = format!(
757 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
758 content.len()
759 );
760 let mut data = header.into_bytes();
761 data.extend_from_slice(content);
762 data.extend_from_slice(b"\x0B\n");
763
764 let mut iter = ParsedMessageIterator::new(&data[..]);
765 let parsed: Vec<_> = iter.by_ref().collect::<Result<Vec<_>, _>>().unwrap();
766 assert_eq!(parsed.len(), 1);
767 let stats = iter.parse_stats();
768 assert_eq!(stats.bytes_read, data.len() as u64);
769 assert_eq!(stats.bytes_skipped, 0);
770 }
771
772 #[test]
773 fn parse_options_request() {
774 let content = b"OPTIONS sip:user@host SIP/2.0\r\n\
775 Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-1\r\n\
776 From: <sip:user@host>;tag=abc\r\n\
777 To: <sip:user@host>\r\n\
778 Call-ID: test-call-id@host\r\n\
779 CSeq: 1 OPTIONS\r\n\
780 Content-Length: 0\r\n\
781 \r\n";
782 let msg = make_sip_message(content);
783 let parsed = msg.parse().unwrap();
784
785 assert_eq!(
786 parsed.message_type,
787 SipMessageType::Request {
788 method: "OPTIONS".into(),
789 uri: "sip:user@host".into()
790 }
791 );
792 assert_eq!(parsed.call_id(), Some("test-call-id@host"));
793 assert_eq!(parsed.cseq(), Some("1 OPTIONS"));
794 assert_eq!(parsed.content_length(), Some(0));
795 assert_eq!(parsed.method(), Some("OPTIONS"));
796 assert!(parsed.body.is_empty());
797 }
798
799 #[test]
800 fn parse_200_ok_response() {
801 let content = b"SIP/2.0 200 OK\r\n\
802 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
803 Call-ID: resp-id@host\r\n\
804 CSeq: 1 INVITE\r\n\
805 Content-Length: 0\r\n\
806 \r\n";
807 let msg = make_sip_message(content);
808 let parsed = msg.parse().unwrap();
809
810 assert_eq!(
811 parsed.message_type,
812 SipMessageType::Response {
813 code: 200,
814 reason: "OK".into()
815 }
816 );
817 assert_eq!(parsed.method(), Some("INVITE"));
818 }
819
820 #[test]
821 fn parse_100_trying() {
822 let content = b"SIP/2.0 100 Trying\r\n\
823 Via: SIP/2.0/TCP 10.0.0.1:5060\r\n\
824 Call-ID: trying-id\r\n\
825 CSeq: 42 INVITE\r\n\
826 Content-Length: 0\r\n\
827 \r\n";
828 let msg = make_sip_message(content);
829 let parsed = msg.parse().unwrap();
830
831 assert_eq!(
832 parsed.message_type,
833 SipMessageType::Response {
834 code: 100,
835 reason: "Trying".into()
836 }
837 );
838 assert_eq!(parsed.method(), Some("INVITE"));
839 }
840
841 #[test]
842 fn parse_invite_with_sdp_body() {
843 let body = b"v=0\r\no=- 123 456 IN IP4 10.0.0.1\r\ns=-\r\n";
844 let mut content = Vec::new();
845 content.extend_from_slice(b"INVITE sip:user@host SIP/2.0\r\n");
846 content.extend_from_slice(b"Call-ID: invite-body@host\r\n");
847 content.extend_from_slice(b"CSeq: 1 INVITE\r\n");
848 content.extend_from_slice(b"Content-Type: application/sdp\r\n");
849 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
850 content.extend_from_slice(b"\r\n");
851 content.extend_from_slice(body);
852
853 let msg = make_sip_message(&content);
854 let parsed = msg.parse().unwrap();
855
856 assert_eq!(parsed.method(), Some("INVITE"));
857 assert_eq!(parsed.content_type(), Some("application/sdp"));
858 assert_eq!(parsed.content_length(), Some(body.len()));
859 assert_eq!(parsed.body, body);
860 }
861
862 #[test]
863 fn parse_notify_with_json_body() {
864 let body = br#"{"event":"AbandonedCall","id":"123"}"#;
865 let mut content = Vec::new();
866 content.extend_from_slice(b"NOTIFY sip:user@host SIP/2.0\r\n");
867 content.extend_from_slice(b"Call-ID: notify-json@host\r\n");
868 content.extend_from_slice(b"CSeq: 1 NOTIFY\r\n");
869 content.extend_from_slice(b"Content-Type: application/json\r\n");
870 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
871 content.extend_from_slice(b"\r\n");
872 content.extend_from_slice(body);
873
874 let msg = make_sip_message(&content);
875 let parsed = msg.parse().unwrap();
876
877 assert_eq!(parsed.method(), Some("NOTIFY"));
878 assert_eq!(parsed.content_type(), Some("application/json"));
879 assert_eq!(parsed.body, body);
880 }
881
882 #[test]
883 fn compact_headers() {
884 let content = b"NOTIFY sip:user@host SIP/2.0\r\n\
885 i: compact-call-id\r\n\
886 l: 0\r\n\
887 c: text/plain\r\n\
888 \r\n";
889 let msg = make_sip_message(content);
890 let parsed = msg.parse().unwrap();
891
892 assert_eq!(parsed.call_id(), Some("compact-call-id"));
893 assert_eq!(parsed.content_length(), Some(0));
894 assert_eq!(parsed.content_type(), Some("text/plain"));
895 }
896
897 #[test]
898 fn header_folding() {
899 let content = b"OPTIONS sip:host SIP/2.0\r\n\
900 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
901 Subject: this is a long\r\n \
902 folded header value\r\n\
903 Call-ID: fold-test\r\n\
904 Content-Length: 0\r\n\
905 \r\n";
906 let msg = make_sip_message(content);
907 let parsed = msg.parse().unwrap();
908
909 let subject = parsed
910 .headers
911 .iter()
912 .find(|(k, _)| k == "Subject")
913 .map(|(_, v)| v.as_str());
914 assert_eq!(subject, Some("this is a long folded header value"));
915 assert_eq!(parsed.call_id(), Some("fold-test"));
916 }
917
918 #[test]
919 fn folded_header_no_crlf_leak() {
920 let content = b"OPTIONS sip:host SIP/2.0\r\n\
921 Subject: line1\r\n \
922 line2\r\n\
923 Content-Length: 0\r\n\
924 \r\n";
925 let msg = make_sip_message(content);
926 let parsed = msg.parse().unwrap();
927 let subject = parsed.headers.iter().find(|(k, _)| k == "Subject").unwrap();
928 assert!(!subject.1.contains('\r'), "CRLF leaked: {:?}", subject.1);
929 assert!(!subject.1.contains('\n'), "LF leaked: {:?}", subject.1);
930 assert_eq!(subject.1, "line1 line2");
931 }
932
933 #[test]
934 fn no_body() {
935 let content = b"OPTIONS sip:host SIP/2.0\r\n\
936 Call-ID: nobody\r\n\
937 Content-Length: 0\r\n\
938 \r\n";
939 let msg = make_sip_message(content);
940 let parsed = msg.parse().unwrap();
941 assert!(parsed.body.is_empty());
942 }
943
944 #[test]
945 fn no_blank_line_no_body() {
946 let content = b"OPTIONS sip:host SIP/2.0\r\n\
948 Call-ID: no-blank\r\n\
949 Content-Length: 0";
950 let msg = make_sip_message(content);
951 let parsed = msg.parse().unwrap();
952 assert!(parsed.body.is_empty());
953 assert_eq!(parsed.call_id(), Some("no-blank"));
954 }
955
956 #[test]
957 fn preserves_metadata() {
958 let content = b"REGISTER sip:host SIP/2.0\r\n\
959 Call-ID: meta-test\r\n\
960 \r\n";
961 let msg = SipMessage {
962 direction: Direction::Sent,
963 transport: Transport::Tls,
964 address: "[2001:db8::1]:5061".into(),
965 timestamp: Timestamp::DateTime {
966 year: 2026,
967 month: 2,
968 day: 12,
969 hour: 10,
970 min: 30,
971 sec: 0,
972 usec: 123456,
973 },
974 content: content.to_vec(),
975 frame_count: 3,
976 };
977 let parsed = msg.parse().unwrap();
978
979 assert_eq!(parsed.direction, Direction::Sent);
980 assert_eq!(parsed.transport, Transport::Tls);
981 assert_eq!(parsed.address, "[2001:db8::1]:5061");
982 assert_eq!(parsed.frame_count, 3);
983 assert_eq!(
984 parsed.timestamp,
985 Timestamp::DateTime {
986 year: 2026,
987 month: 2,
988 day: 12,
989 hour: 10,
990 min: 30,
991 sec: 0,
992 usec: 123456,
993 }
994 );
995 }
996
997 #[test]
998 fn multiple_same_name_headers() {
999 let content = b"INVITE sip:host SIP/2.0\r\n\
1000 Via: SIP/2.0/UDP proxy1:5060\r\n\
1001 Via: SIP/2.0/UDP proxy2:5060\r\n\
1002 Record-Route: <sip:proxy1>\r\n\
1003 Record-Route: <sip:proxy2>\r\n\
1004 Call-ID: multi-hdr\r\n\
1005 Content-Length: 0\r\n\
1006 \r\n";
1007 let msg = make_sip_message(content);
1008 let parsed = msg.parse().unwrap();
1009
1010 let via_count = parsed.headers.iter().filter(|(k, _)| k == "Via").count();
1011 assert_eq!(via_count, 2);
1012
1013 let rr_count = parsed
1014 .headers
1015 .iter()
1016 .filter(|(k, _)| k == "Record-Route")
1017 .count();
1018 assert_eq!(rr_count, 2);
1019 }
1020
1021 #[test]
1022 fn header_ordering_preserved() {
1023 let content = b"OPTIONS sip:host SIP/2.0\r\n\
1024 Via: v1\r\n\
1025 From: f1\r\n\
1026 To: t1\r\n\
1027 Call-ID: order-test\r\n\
1028 CSeq: 1 OPTIONS\r\n\
1029 \r\n";
1030 let msg = make_sip_message(content);
1031 let parsed = msg.parse().unwrap();
1032
1033 let names: Vec<&str> = parsed.headers.iter().map(|(k, _)| k.as_str()).collect();
1034 assert_eq!(names, vec!["Via", "From", "To", "Call-ID", "CSeq"]);
1035 }
1036
1037 #[test]
1038 fn status_line_with_long_reason() {
1039 let content = b"SIP/2.0 486 Busy Here\r\n\
1040 Call-ID: busy\r\n\
1041 \r\n";
1042 let msg = make_sip_message(content);
1043 let parsed = msg.parse().unwrap();
1044
1045 assert_eq!(
1046 parsed.message_type,
1047 SipMessageType::Response {
1048 code: 486,
1049 reason: "Busy Here".into()
1050 }
1051 );
1052 }
1053
1054 #[test]
1055 fn request_with_complex_uri() {
1056 let content = b"INVITE sip:+15551234567@gateway.example.com;transport=tcp SIP/2.0\r\n\
1057 Call-ID: complex-uri\r\n\
1058 \r\n";
1059 let msg = make_sip_message(content);
1060 let parsed = msg.parse().unwrap();
1061
1062 assert_eq!(
1063 parsed.message_type,
1064 SipMessageType::Request {
1065 method: "INVITE".into(),
1066 uri: "sip:+15551234567@gateway.example.com;transport=tcp".into()
1067 }
1068 );
1069 }
1070
1071 #[test]
1072 fn binary_body() {
1073 let body: Vec<u8> = (0..256).map(|i| i as u8).collect();
1074 let mut content = Vec::new();
1075 content.extend_from_slice(b"MESSAGE sip:host SIP/2.0\r\n");
1076 content.extend_from_slice(b"Call-ID: binary-body\r\n");
1077 content.extend_from_slice(b"Content-Type: application/octet-stream\r\n");
1078 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1079 content.extend_from_slice(b"\r\n");
1080 content.extend_from_slice(&body);
1081
1082 let msg = make_sip_message(&content);
1083 let parsed = msg.parse().unwrap();
1084
1085 assert_eq!(parsed.body, body);
1086 }
1087
1088 #[test]
1089 fn error_no_crlf() {
1090 let content = b"garbage without any crlf";
1091 let msg = make_sip_message(content);
1092 let result = msg.parse();
1093 assert!(result.is_err());
1094 }
1095
1096 #[test]
1097 fn error_no_space_in_request_line() {
1098 let content = b"INVALID\r\n\r\n";
1099 let msg = make_sip_message(content);
1100 let result = msg.parse();
1101 assert!(result.is_err());
1102 }
1103
1104 #[test]
1105 fn parse_request_rejects_xml_method() {
1106 let content =
1107 b"</confInfo:conference-info>NOTIFY sip:user@host SIP/2.0\r\nContent-Length: 0\r\n\r\n";
1108 let msg = make_sip_message(content);
1109 assert!(msg.parse().is_err(), "should reject XML-prefixed method");
1110 }
1111
1112 #[test]
1113 fn parse_request_rejects_method_with_angle_brackets() {
1114 let content = b"<xml>BYE sip:host SIP/2.0\r\n\r\n";
1115 let msg = make_sip_message(content);
1116 assert!(msg.parse().is_err());
1117 }
1118
1119 #[test]
1120 fn parse_request_accepts_extension_method() {
1121 let content = b"CUSTOM-METHOD sip:host SIP/2.0\r\nContent-Length: 0\r\n\r\n";
1122 let msg = make_sip_message(content);
1123 let parsed = msg.parse().unwrap();
1124 assert_eq!(
1125 parsed.message_type,
1126 SipMessageType::Request {
1127 method: "CUSTOM-METHOD".into(),
1128 uri: "sip:host".into()
1129 }
1130 );
1131 }
1132
1133 #[test]
1134 fn header_value_with_colon() {
1135 let content = b"INVITE sip:host SIP/2.0\r\n\
1137 Contact: <sip:user@10.0.0.1:5060;transport=tcp>\r\n\
1138 Call-ID: colon-val\r\n\
1139 \r\n";
1140 let msg = make_sip_message(content);
1141 let parsed = msg.parse().unwrap();
1142
1143 let contact = parsed
1144 .headers
1145 .iter()
1146 .find(|(k, _)| k == "Contact")
1147 .map(|(_, v)| v.as_str());
1148 assert_eq!(contact, Some("<sip:user@10.0.0.1:5060;transport=tcp>"));
1149 }
1150
1151 #[test]
1152 fn whitespace_around_header_value() {
1153 let content = b"OPTIONS sip:host SIP/2.0\r\n\
1154 Call-ID: spaces-around \r\n\
1155 \r\n";
1156 let msg = make_sip_message(content);
1157 let parsed = msg.parse().unwrap();
1158
1159 assert_eq!(parsed.call_id(), Some("spaces-around "));
1161 }
1162
1163 #[test]
1164 fn parsed_message_iterator() {
1165 let content =
1166 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: iter-test\r\nContent-Length: 0\r\n\r\n";
1167 let header = format!(
1168 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
1169 content.len()
1170 );
1171 let mut data = header.into_bytes();
1172 data.extend_from_slice(content);
1173 data.extend_from_slice(b"\x0B\n");
1174
1175 let parsed: Vec<ParsedSipMessage> = ParsedMessageIterator::new(&data[..])
1176 .collect::<Result<Vec<_>, _>>()
1177 .unwrap();
1178
1179 assert_eq!(parsed.len(), 1);
1180 assert_eq!(parsed[0].call_id(), Some("iter-test"));
1181 assert_eq!(parsed[0].method(), Some("OPTIONS"));
1182 }
1183
1184 fn make_multipart_invite(boundary: &str, parts: &[(&str, &[u8])]) -> SipMessage {
1187 let mut body = Vec::new();
1188 for (ct, content) in parts {
1189 body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
1190 body.extend_from_slice(format!("Content-Type: {ct}\r\n").as_bytes());
1191 body.extend_from_slice(b"\r\n");
1192 body.extend_from_slice(content);
1193 body.extend_from_slice(b"\r\n");
1194 }
1195 body.extend_from_slice(format!("--{boundary}--").as_bytes());
1196
1197 let mut content = Vec::new();
1198 content.extend_from_slice(b"INVITE sip:urn:service:sos@esrp.example.com SIP/2.0\r\n");
1199 content.extend_from_slice(b"Call-ID: multipart-test@host\r\n");
1200 content.extend_from_slice(b"CSeq: 1 INVITE\r\n");
1201 content.extend_from_slice(
1202 format!("Content-Type: multipart/mixed;boundary={boundary}\r\n").as_bytes(),
1203 );
1204 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1205 content.extend_from_slice(b"\r\n");
1206 content.extend_from_slice(&body);
1207
1208 make_sip_message(&content)
1209 }
1210
1211 #[test]
1212 fn multipart_sdp_and_pidf() {
1213 let sdp = b"v=0\r\no=- 123 456 IN IP4 10.0.0.1\r\ns=-\r\n";
1214 let pidf = b"<?xml version=\"1.0\"?>\r\n<presence xmlns=\"urn:ietf:params:xml:ns:pidf\"/>";
1215 let msg = make_multipart_invite(
1216 "unique-boundary-1",
1217 &[("application/sdp", sdp), ("application/pidf+xml", pidf)],
1218 );
1219 let parsed = msg.parse().unwrap();
1220
1221 assert!(parsed.is_multipart());
1222 assert_eq!(parsed.multipart_boundary(), Some("unique-boundary-1"));
1223
1224 let parts = parsed.body_parts().unwrap();
1225 assert_eq!(parts.len(), 2);
1226
1227 assert_eq!(parts[0].content_type(), Some("application/sdp"));
1228 assert_eq!(parts[0].body, sdp);
1229
1230 assert_eq!(parts[1].content_type(), Some("application/pidf+xml"));
1231 assert_eq!(parts[1].body, pidf);
1232 }
1233
1234 #[test]
1235 fn multipart_sdp_and_eido() {
1236 let sdp = b"v=0\r\no=- 1 1 IN IP4 10.0.0.1\r\ns=-\r\n\
1237 c=IN IP4 10.0.0.1\r\nt=0 0\r\nm=audio 8000 RTP/AVP 0\r\n";
1238 let eido = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\
1239 <eido:EmergencyCallData xmlns:eido=\"urn:nena:xml:ns:EmergencyCallData\">\r\n\
1240 <eido:IncidentId>INC-2026-001</eido:IncidentId>\r\n\
1241 </eido:EmergencyCallData>";
1242 let msg = make_multipart_invite(
1243 "ng911-boundary",
1244 &[
1245 ("application/sdp", sdp),
1246 ("application/emergencyCallData.eido+xml", eido),
1247 ],
1248 );
1249 let parsed = msg.parse().unwrap();
1250 let parts = parsed.body_parts().unwrap();
1251 assert_eq!(parts.len(), 2);
1252
1253 let sdp_part = parts
1254 .iter()
1255 .find(|p| p.content_type() == Some("application/sdp"));
1256 assert!(sdp_part.is_some());
1257 assert_eq!(sdp_part.unwrap().body, sdp);
1258
1259 let eido_part = parts
1260 .iter()
1261 .find(|p| p.content_type().is_some_and(|ct| ct.contains("eido")));
1262 assert!(eido_part.is_some());
1263 assert_eq!(eido_part.unwrap().body, eido);
1264 }
1265
1266 #[test]
1267 fn multipart_three_parts_sdp_pidf_eido() {
1268 let sdp = b"v=0\r\ns=-\r\n";
1269 let pidf = b"<presence/>";
1270 let eido = b"<EmergencyCallData/>";
1271 let msg = make_multipart_invite(
1272 "tri-part",
1273 &[
1274 ("application/sdp", sdp),
1275 ("application/pidf+xml", pidf),
1276 ("application/emergencyCallData.eido+xml", eido),
1277 ],
1278 );
1279 let parsed = msg.parse().unwrap();
1280 let parts = parsed.body_parts().unwrap();
1281 assert_eq!(parts.len(), 3);
1282 assert_eq!(parts[0].content_type(), Some("application/sdp"));
1283 assert_eq!(parts[1].content_type(), Some("application/pidf+xml"));
1284 assert_eq!(
1285 parts[2].content_type(),
1286 Some("application/emergencyCallData.eido+xml")
1287 );
1288 }
1289
1290 #[test]
1291 fn multipart_quoted_boundary() {
1292 let sdp = b"v=0\r\n";
1293 let pidf = b"<presence/>";
1294
1295 let mut body = Vec::new();
1296 body.extend_from_slice(b"--quoted-boundary\r\n");
1297 body.extend_from_slice(b"Content-Type: application/sdp\r\n\r\n");
1298 body.extend_from_slice(sdp);
1299 body.extend_from_slice(b"\r\n--quoted-boundary\r\n");
1300 body.extend_from_slice(b"Content-Type: application/pidf+xml\r\n\r\n");
1301 body.extend_from_slice(pidf);
1302 body.extend_from_slice(b"\r\n--quoted-boundary--");
1303
1304 let mut content = Vec::new();
1305 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1306 content.extend_from_slice(b"Call-ID: quoted-bnd@host\r\n");
1307 content
1308 .extend_from_slice(b"Content-Type: multipart/mixed; boundary=\"quoted-boundary\"\r\n");
1309 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1310 content.extend_from_slice(b"\r\n");
1311 content.extend_from_slice(&body);
1312
1313 let msg = make_sip_message(&content);
1314 let parsed = msg.parse().unwrap();
1315
1316 assert_eq!(parsed.multipart_boundary(), Some("quoted-boundary"));
1317 let parts = parsed.body_parts().unwrap();
1318 assert_eq!(parts.len(), 2);
1319 assert_eq!(parts[0].body, sdp);
1320 assert_eq!(parts[1].body, pidf);
1321 }
1322
1323 #[test]
1324 fn multipart_with_preamble() {
1325 let sdp = b"v=0\r\n";
1326
1327 let mut body = Vec::new();
1328 body.extend_from_slice(b"This is the preamble. It should be ignored.\r\n");
1329 body.extend_from_slice(b"--boundary-pre\r\n");
1330 body.extend_from_slice(b"Content-Type: application/sdp\r\n\r\n");
1331 body.extend_from_slice(sdp);
1332 body.extend_from_slice(b"\r\n--boundary-pre--");
1333
1334 let mut content = Vec::new();
1335 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1336 content.extend_from_slice(b"Call-ID: preamble@host\r\n");
1337 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=boundary-pre\r\n");
1338 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1339 content.extend_from_slice(b"\r\n");
1340 content.extend_from_slice(&body);
1341
1342 let msg = make_sip_message(&content);
1343 let parsed = msg.parse().unwrap();
1344 let parts = parsed.body_parts().unwrap();
1345 assert_eq!(parts.len(), 1);
1346 assert_eq!(parts[0].body, sdp);
1347 }
1348
1349 #[test]
1350 fn multipart_part_with_multiple_headers() {
1351 let eido = b"<EmergencyCallData/>";
1352
1353 let mut body = Vec::new();
1354 body.extend_from_slice(b"--hdr-boundary\r\n");
1355 body.extend_from_slice(b"Content-Type: application/emergencyCallData.eido+xml\r\n");
1356 body.extend_from_slice(b"Content-ID: <eido@example.com>\r\n");
1357 body.extend_from_slice(b"Content-Disposition: by-reference\r\n");
1358 body.extend_from_slice(b"\r\n");
1359 body.extend_from_slice(eido);
1360 body.extend_from_slice(b"\r\n--hdr-boundary--");
1361
1362 let mut content = Vec::new();
1363 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1364 content.extend_from_slice(b"Call-ID: multi-hdr-part@host\r\n");
1365 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=hdr-boundary\r\n");
1366 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1367 content.extend_from_slice(b"\r\n");
1368 content.extend_from_slice(&body);
1369
1370 let msg = make_sip_message(&content);
1371 let parsed = msg.parse().unwrap();
1372 let parts = parsed.body_parts().unwrap();
1373 assert_eq!(parts.len(), 1);
1374 assert_eq!(
1375 parts[0].content_type(),
1376 Some("application/emergencyCallData.eido+xml")
1377 );
1378 assert_eq!(parts[0].content_id(), Some("<eido@example.com>"));
1379 assert_eq!(parts[0].content_disposition(), Some("by-reference"));
1380 assert_eq!(parts[0].body, eido);
1381 }
1382
1383 #[test]
1384 fn not_multipart_returns_none() {
1385 let content = b"INVITE sip:host SIP/2.0\r\n\
1386 Call-ID: not-multi@host\r\n\
1387 Content-Type: application/sdp\r\n\
1388 Content-Length: 4\r\n\
1389 \r\n\
1390 v=0\n";
1391 let msg = make_sip_message(content);
1392 let parsed = msg.parse().unwrap();
1393
1394 assert!(!parsed.is_multipart());
1395 assert!(parsed.multipart_boundary().is_none());
1396 assert!(parsed.body_parts().is_none());
1397 }
1398
1399 #[test]
1400 fn multipart_empty_body() {
1401 let mut content = Vec::new();
1402 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1403 content.extend_from_slice(b"Call-ID: empty-multi@host\r\n");
1404 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=empty\r\n");
1405 content.extend_from_slice(b"Content-Length: 9\r\n");
1406 content.extend_from_slice(b"\r\n");
1407 content.extend_from_slice(b"--empty--");
1408
1409 let msg = make_sip_message(&content);
1410 let parsed = msg.parse().unwrap();
1411 assert!(parsed.body_parts().is_none());
1412
1413 let parts = parsed.body_as_parts();
1414 assert_eq!(parts.len(), 1);
1415 assert_eq!(parts[0].body, b"--empty--");
1416 }
1417
1418 #[test]
1419 fn extract_boundary_unquoted() {
1420 assert_eq!(
1421 extract_boundary("multipart/mixed;boundary=foo-bar"),
1422 Some("foo-bar")
1423 );
1424 }
1425
1426 #[test]
1427 fn extract_boundary_quoted() {
1428 assert_eq!(
1429 extract_boundary("multipart/mixed; boundary=\"foo-bar\""),
1430 Some("foo-bar")
1431 );
1432 }
1433
1434 #[test]
1435 fn extract_boundary_with_extra_params() {
1436 assert_eq!(
1437 extract_boundary("multipart/mixed; boundary=foo;charset=utf-8"),
1438 Some("foo")
1439 );
1440 }
1441
1442 #[test]
1443 fn extract_boundary_case_insensitive() {
1444 assert_eq!(
1445 extract_boundary("multipart/mixed;BOUNDARY=abc"),
1446 Some("abc")
1447 );
1448 }
1449
1450 #[test]
1451 fn extract_boundary_missing() {
1452 assert_eq!(extract_boundary("multipart/mixed"), None);
1453 }
1454
1455 #[test]
1456 fn multipart_part_no_headers() {
1457 let raw_body = b"just raw content";
1458
1459 let mut body = Vec::new();
1460 body.extend_from_slice(b"--no-hdr\r\n");
1461 body.extend_from_slice(raw_body);
1462 body.extend_from_slice(b"\r\n--no-hdr--");
1463
1464 let mut content = Vec::new();
1465 content.extend_from_slice(b"MESSAGE sip:host SIP/2.0\r\n");
1466 content.extend_from_slice(b"Call-ID: no-hdr-part@host\r\n");
1467 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=no-hdr\r\n");
1468 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1469 content.extend_from_slice(b"\r\n");
1470 content.extend_from_slice(&body);
1471
1472 let msg = make_sip_message(&content);
1473 let parsed = msg.parse().unwrap();
1474 let parts = parsed.body_parts().unwrap();
1475 assert_eq!(parts.len(), 1);
1476 assert!(parts[0].content_type().is_none());
1477 assert!(parts[0].headers.is_empty());
1478 assert_eq!(parts[0].body, raw_body);
1479 }
1480
1481 fn make_with_content_type(header: &str) -> ParsedSipMessage {
1484 let content = format!("INVITE sip:host SIP/2.0\r\n{header}\r\nCall-ID: mt@host\r\n\r\n");
1485 make_sip_message(content.as_bytes()).parse().unwrap()
1486 }
1487
1488 #[test]
1489 fn media_type_strips_parameters() {
1490 let parsed = make_with_content_type("Content-Type: multipart/mixed;boundary=abc");
1491 assert_eq!(parsed.media_type().as_deref(), Some("multipart/mixed"));
1492 }
1493
1494 #[test]
1495 fn media_type_lowercases() {
1496 let parsed = make_with_content_type("Content-Type: Application/SDP");
1497 assert_eq!(parsed.media_type().as_deref(), Some("application/sdp"));
1498 }
1499
1500 #[test]
1501 fn media_type_trims_whitespace() {
1502 let parsed = make_with_content_type("Content-Type: application/sdp ; charset=utf-8");
1503 assert_eq!(parsed.media_type().as_deref(), Some("application/sdp"));
1504 }
1505
1506 #[test]
1507 fn media_type_compact_form() {
1508 let parsed = make_with_content_type("c: application/pidf+xml");
1509 assert_eq!(parsed.media_type().as_deref(), Some("application/pidf+xml"));
1510 }
1511
1512 #[test]
1513 fn media_type_absent() {
1514 let parsed = make_with_content_type("Subject: none");
1515 assert_eq!(parsed.media_type(), None);
1516 }
1517
1518 #[test]
1519 fn media_type_on_mime_part() {
1520 let msg = make_multipart_invite(
1521 "mt-boundary",
1522 &[("Application/SDP; charset=utf-8", b"v=0\r\n")],
1523 );
1524 let parsed = msg.parse().unwrap();
1525 let parts = parsed.body_parts().unwrap();
1526 assert_eq!(parts[0].media_type().as_deref(), Some("application/sdp"));
1527 }
1528
1529 #[test]
1532 fn body_as_parts_wraps_non_multipart() {
1533 let body = b"v=0\r\no=- 1 1 IN IP4 10.0.0.1\r\n";
1534 let mut content = Vec::new();
1535 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1536 content.extend_from_slice(b"Call-ID: abp-plain@host\r\n");
1537 content.extend_from_slice(b"Content-Type: application/sdp\r\n");
1538 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1539 content.extend_from_slice(b"\r\n");
1540 content.extend_from_slice(body);
1541
1542 let parsed = make_sip_message(&content).parse().unwrap();
1543 let parts = parsed.body_as_parts();
1544 assert_eq!(parts.len(), 1);
1545 assert_eq!(parts[0].media_type().as_deref(), Some("application/sdp"));
1546 assert_eq!(parts[0].body, body);
1547 }
1548
1549 #[test]
1550 fn body_as_parts_matches_body_parts_for_multipart() {
1551 let msg = make_multipart_invite(
1552 "abp-multi",
1553 &[
1554 ("application/sdp", b"v=0\r\n"),
1555 ("application/pidf+xml", b"<presence/>"),
1556 ],
1557 );
1558 let parsed = msg.parse().unwrap();
1559 let all = parsed.body_as_parts();
1560 let split = parsed.body_parts().unwrap();
1561 assert_eq!(all.len(), split.len());
1562 for (a, b) in all.iter().zip(split.iter()) {
1563 assert_eq!(a.headers, b.headers);
1564 assert_eq!(a.body, b.body);
1565 }
1566 }
1567
1568 #[test]
1569 fn body_as_parts_empty_body() {
1570 let content = b"OPTIONS sip:host SIP/2.0\r\n\
1571 Call-ID: abp-empty@host\r\n\
1572 Content-Length: 0\r\n\
1573 \r\n";
1574 let parsed = make_sip_message(content).parse().unwrap();
1575 assert!(parsed.body_as_parts().is_empty());
1576 }
1577
1578 #[test]
1579 fn body_as_parts_multipart_without_boundary() {
1580 let body = b"--something\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n";
1581 let mut content = Vec::new();
1582 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1583 content.extend_from_slice(b"Call-ID: abp-nobnd@host\r\n");
1584 content.extend_from_slice(b"Content-Type: multipart/mixed\r\n");
1585 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1586 content.extend_from_slice(b"\r\n");
1587 content.extend_from_slice(body);
1588
1589 let parsed = make_sip_message(&content).parse().unwrap();
1590 let parts = parsed.body_as_parts();
1591 assert_eq!(
1592 parts.len(),
1593 1,
1594 "unsplittable multipart must surface as one part"
1595 );
1596 assert_eq!(parts[0].media_type().as_deref(), Some("multipart/mixed"));
1597 assert_eq!(parts[0].body, body);
1598 }
1599
1600 #[test]
1601 fn body_as_parts_boundary_not_in_body() {
1602 let body = b"--other\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n--other--";
1603 let parsed = parsed_with_headers(
1604 "abp-mismatch",
1605 &["Content-Type: multipart/mixed;boundary=declared"],
1606 body,
1607 );
1608 let parts = parsed.body_as_parts();
1609 assert_eq!(
1610 parts.len(),
1611 1,
1612 "a boundary absent from the body is not a split"
1613 );
1614 assert_eq!(parts[0].media_type().as_deref(), Some("multipart/mixed"));
1615 assert_eq!(parts[0].body, body);
1616 assert!(parsed.body_parts().is_none());
1617 }
1618
1619 #[test]
1620 fn body_as_parts_truncated_multipart() {
1621 let body = b"--trunc\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n";
1622 let parsed = parsed_with_headers(
1623 "abp-trunc",
1624 &["Content-Type: multipart/mixed;boundary=trunc"],
1625 body,
1626 );
1627 let parts = parsed.body_as_parts();
1628 assert_eq!(
1629 parts.len(),
1630 1,
1631 "a body cut off before the closing delimiter must not vanish"
1632 );
1633 assert_eq!(parts[0].content_type(), Some("application/sdp"));
1634 assert_eq!(parts[0].body, b"v=0\r\n");
1635 assert!(parsed.body_parts().is_some());
1636 }
1637
1638 #[test]
1639 fn multipart_truncated_trailing_part() {
1640 let body = b"--b\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n\
1641 --b\r\nContent-Type: application/pidf+xml\r\n\r\n<presence";
1642 let parts = parse_multipart_body(body, "b");
1643 assert_eq!(parts.len(), 2);
1644 assert_eq!(parts[0].body, b"v=0");
1645 assert_eq!(parts[1].content_type(), Some("application/pidf+xml"));
1646 assert_eq!(parts[1].body, b"<presence");
1647 }
1648
1649 #[test]
1650 fn multipart_truncated_inside_close_delimiter() {
1651 let body = b"--b\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n--b";
1652 let parts = parse_multipart_body(body, "b");
1653 assert_eq!(parts.len(), 1);
1654 assert_eq!(parts[0].body, b"v=0");
1655 }
1656
1657 #[test]
1658 fn multipart_preamble_substring_no_false_part() {
1659 let body = b"preamble mentions --b in passing\r\n\
1660 --b\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n--b--";
1661 let parts = parse_multipart_body(body, "b");
1662 assert_eq!(parts.len(), 1);
1663 assert_eq!(parts[0].content_type(), Some("application/sdp"));
1664 assert_eq!(parts[0].body, b"v=0");
1665 }
1666
1667 #[test]
1668 fn multipart_boundary_prefix_collision() {
1669 let body =
1670 b"--b\r\nContent-Type: text/plain\r\n\r\nouter\r\n--b2\r\ninner text\r\n--b2--\r\n--b--";
1671 let parts = parse_multipart_body(body, "b");
1672 assert_eq!(parts.len(), 1);
1673 assert_eq!(parts[0].body, b"outer\r\n--b2\r\ninner text\r\n--b2--");
1674 }
1675
1676 #[test]
1677 fn multipart_delimiter_transport_padding() {
1678 let body = b"--b \t\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n\
1679 --b \r\nContent-Type: application/pidf+xml\r\n\r\n<presence/>\r\n--b--";
1680 let parts = parse_multipart_body(body, "b");
1681 assert_eq!(parts.len(), 2);
1682 assert_eq!(parts[0].body, b"v=0");
1683 assert_eq!(parts[1].body, b"<presence/>");
1684 }
1685
1686 #[test]
1687 fn multipart_no_preamble_delimiter_at_offset_zero() {
1688 let body = b"--b\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n--b--";
1689 let parts = parse_multipart_body(body, "b");
1690 assert_eq!(parts.len(), 1);
1691 assert_eq!(parts[0].body, b"v=0");
1692 }
1693
1694 fn parsed_with_headers(call_id: &str, headers: &[&str], body: &[u8]) -> ParsedSipMessage {
1697 let mut content = Vec::new();
1698 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1699 content.extend_from_slice(format!("Call-ID: {call_id}@host\r\n").as_bytes());
1700 for header in headers {
1701 content.extend_from_slice(header.as_bytes());
1702 content.extend_from_slice(b"\r\n");
1703 }
1704 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1705 content.extend_from_slice(b"\r\n");
1706 content.extend_from_slice(body);
1707 make_sip_message(&content).parse().unwrap()
1708 }
1709
1710 fn part_header<'a>(part: &'a MimePart, name: &str) -> Option<&'a str> {
1711 part.headers
1712 .iter()
1713 .find(|(k, _)| k.eq_ignore_ascii_case(name))
1714 .map(|(_, v)| v.as_str())
1715 }
1716
1717 #[test]
1718 fn synthetic_part_carries_transfer_encoding() {
1719 let parsed = parsed_with_headers(
1720 "abp-cte",
1721 &[
1722 "Content-Type: application/pidf+xml",
1723 "Content-Transfer-Encoding: base64",
1724 ],
1725 b"PD94bWwgdmVyc2lvbj0iMS4wIj8+",
1726 );
1727 let parts = parsed.body_as_parts();
1728 assert_eq!(
1729 parts[0].content_transfer_encoding(),
1730 Some("base64"),
1731 "a per-part consumer must see the encoding the message declared"
1732 );
1733 }
1734
1735 #[test]
1736 fn synthetic_part_carries_disposition_and_id() {
1737 let parsed = parsed_with_headers(
1738 "abp-cd",
1739 &[
1740 "Content-Type: application/sdp",
1741 "Content-Disposition: session",
1742 "Content-ID: <sdp@host>",
1743 ],
1744 b"v=0\r\n",
1745 );
1746 let parts = parsed.body_as_parts();
1747 assert_eq!(parts[0].content_disposition(), Some("session"));
1748 assert_eq!(parts[0].content_id(), Some("<sdp@host>"));
1749 }
1750
1751 #[test]
1752 fn synthetic_part_canonicalizes_compact_content_encoding() {
1753 let parsed = parsed_with_headers(
1754 "abp-compact-e",
1755 &["Content-Type: application/sdp", "e: gzip"],
1756 b"v=0\r\n",
1757 );
1758 let parts = parsed.body_as_parts();
1759 assert_eq!(
1760 part_header(&parts[0], "Content-Encoding"),
1761 Some("gzip"),
1762 "compact form must arrive under the canonical name"
1763 );
1764 }
1765
1766 #[test]
1767 fn synthetic_part_canonicalizes_compact_content_type() {
1768 let parsed = parsed_with_headers("abp-compact-c", &["c: application/sdp"], b"v=0\r\n");
1769 let parts = parsed.body_as_parts();
1770 assert_eq!(parts[0].media_type().as_deref(), Some("application/sdp"));
1771 assert_eq!(
1772 parts[0]
1773 .headers
1774 .iter()
1775 .filter(|(k, _)| k.eq_ignore_ascii_case("Content-Type"))
1776 .count(),
1777 1
1778 );
1779 }
1780
1781 #[test]
1782 fn synthetic_part_content_type_matches_the_message() {
1783 let parsed = parsed_with_headers(
1784 "abp-both-ct",
1785 &["c: text/plain", "Content-Type: application/sdp"],
1786 b"v=0\r\n",
1787 );
1788 let parts = parsed.body_as_parts();
1789 assert_eq!(parts[0].content_type(), parsed.content_type());
1790 assert_eq!(
1791 parts[0]
1792 .headers
1793 .iter()
1794 .filter(|(k, _)| k.eq_ignore_ascii_case("Content-Type"))
1795 .count(),
1796 1
1797 );
1798 }
1799
1800 #[test]
1801 fn synthetic_part_omits_content_length() {
1802 let parsed = parsed_with_headers("abp-len", &["Content-Type: application/sdp"], b"v=0\r\n");
1803 let parts = parsed.body_as_parts();
1804 assert_eq!(part_header(&parts[0], "Content-Length"), None);
1805 assert_eq!(part_header(&parts[0], "l"), None);
1806 }
1807
1808 #[test]
1809 fn synthetic_part_copies_only_content_headers() {
1810 let parsed = parsed_with_headers(
1811 "abp-other",
1812 &[
1813 "Content-Type: application/sdp",
1814 "Subject: not a body header",
1815 ],
1816 b"v=0\r\n",
1817 );
1818 let parts = parsed.body_as_parts();
1819 assert_eq!(part_header(&parts[0], "Subject"), None);
1820 assert_eq!(part_header(&parts[0], "Call-ID"), None);
1821 }
1822
1823 #[test]
1824 fn wire_parts_get_no_fabricated_headers() {
1825 let msg = make_multipart_invite("wire-hdrs", &[("application/sdp", b"v=0\r\n")]);
1826 let mut content = msg.content.clone();
1827 let insert_at = CRLF.find(&content).unwrap() + 2;
1828 content.splice(
1829 insert_at..insert_at,
1830 b"Content-Transfer-Encoding: base64\r\n".iter().copied(),
1831 );
1832 let parsed = make_sip_message(&content).parse().unwrap();
1833 let parts = parsed.body_as_parts();
1834 assert_eq!(parts.len(), 1);
1835 assert_eq!(
1836 parts[0].content_transfer_encoding(),
1837 None,
1838 "a wire part carries what the sender wrote, nothing copied down"
1839 );
1840 }
1841
1842 fn make_nested_multipart_invite() -> SipMessage {
1847 let mut inner = Vec::new();
1848 inner.extend_from_slice(b"--inner\r\n");
1849 inner.extend_from_slice(b"Content-Type: application/pidf+xml\r\n\r\n");
1850 inner.extend_from_slice(b"<presence/>");
1851 inner.extend_from_slice(b"\r\n--inner--");
1852
1853 let mut body = Vec::new();
1854 body.extend_from_slice(b"--outer\r\n");
1855 body.extend_from_slice(b"Content-Type: application/sdp\r\n\r\n");
1856 body.extend_from_slice(b"v=0\r\n");
1857 body.extend_from_slice(b"\r\n--outer\r\n");
1858 body.extend_from_slice(b"Content-Type: multipart/mixed;boundary=inner\r\n\r\n");
1859 body.extend_from_slice(&inner);
1860 body.extend_from_slice(b"\r\n--outer--");
1861
1862 let mut content = Vec::new();
1863 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1864 content.extend_from_slice(b"Call-ID: nested@host\r\n");
1865 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=outer\r\n");
1866 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1867 content.extend_from_slice(b"\r\n");
1868 content.extend_from_slice(&body);
1869
1870 make_sip_message(&content)
1871 }
1872
1873 #[test]
1874 fn nested_multipart_not_flattened() {
1875 let parsed = make_nested_multipart_invite().parse().unwrap();
1876 let parts = parsed.body_as_parts();
1877 assert_eq!(parts.len(), 2);
1878 assert_eq!(parts[0].media_type().as_deref(), Some("application/sdp"));
1879 assert_eq!(parts[1].media_type().as_deref(), Some("multipart/mixed"));
1880 }
1881
1882 #[test]
1883 fn nested_multipart_explicit_descent() {
1884 let parsed = make_nested_multipart_invite().parse().unwrap();
1885 let outer = parsed.body_as_parts();
1886 let nested = &outer[1];
1887
1888 assert!(nested.is_multipart());
1889 assert_eq!(nested.multipart_boundary(), Some("inner"));
1890
1891 let inner = nested.body_parts().unwrap();
1892 assert_eq!(inner.len(), 1);
1893 assert_eq!(
1894 inner[0].media_type().as_deref(),
1895 Some("application/pidf+xml")
1896 );
1897 assert_eq!(inner[0].body, b"<presence/>");
1898 }
1899
1900 #[test]
1901 fn non_multipart_part_has_no_children() {
1902 let parsed = make_nested_multipart_invite().parse().unwrap();
1903 let sdp_part = &parsed.body_as_parts()[0];
1904 assert!(!sdp_part.is_multipart());
1905 assert!(sdp_part.multipart_boundary().is_none());
1906 assert!(sdp_part.body_parts().is_none());
1907 }
1908
1909 #[test]
1912 fn sipfrag_status_line_with_crlf() {
1913 let frag = parse_sipfrag(b"SIP/2.0 200 OK\r\n").unwrap();
1914 assert_eq!(
1915 frag.message_type,
1916 Some(SipMessageType::Response {
1917 code: 200,
1918 reason: "OK".into()
1919 })
1920 );
1921 assert!(frag.headers.is_empty());
1922 assert!(frag.body.is_empty());
1923 }
1924
1925 #[test]
1926 fn sipfrag_status_line_without_trailing_crlf() {
1927 let frag = parse_sipfrag(b"SIP/2.0 183 Session Progress").unwrap();
1928 assert_eq!(
1929 frag.message_type,
1930 Some(SipMessageType::Response {
1931 code: 183,
1932 reason: "Session Progress".into()
1933 })
1934 );
1935 }
1936
1937 #[test]
1938 fn sipfrag_headers_only() {
1939 let frag = parse_sipfrag(b"To: <sip:user@host>\r\nCSeq: 1 INVITE\r\n").unwrap();
1940 assert_eq!(frag.message_type, None);
1941 assert_eq!(frag.headers.len(), 2);
1942 assert_eq!(frag.headers[0].0, "To");
1943 assert_eq!(frag.headers[0].1, "<sip:user@host>");
1944 assert_eq!(frag.headers[1].1, "1 INVITE");
1945 }
1946
1947 #[test]
1948 fn sipfrag_header_value_is_case_insensitive() {
1949 let frag = parse_sipfrag(b"To: <sip:user@host>\r\nCSeq: 1 INVITE\r\n").unwrap();
1950 assert_eq!(frag.header_value("cseq"), Some("1 INVITE"));
1951 assert_eq!(frag.header_value("To"), Some("<sip:user@host>"));
1952 assert_eq!(frag.header_value("Call-ID"), None);
1953 }
1954
1955 #[test]
1956 fn sipfrag_headers_only_without_trailing_crlf() {
1957 let frag = parse_sipfrag(b"To: <sip:user@host>").unwrap();
1958 assert_eq!(frag.message_type, None);
1959 assert_eq!(frag.headers.len(), 1);
1960 assert_eq!(frag.headers[0].1, "<sip:user@host>");
1961 }
1962
1963 #[test]
1964 fn sipfrag_start_line_headers_and_body() {
1965 let data = b"SIP/2.0 200 OK\r\n\
1966 Content-Type: application/sdp\r\n\
1967 \r\n\
1968 v=0\r\n";
1969 let frag = parse_sipfrag(data).unwrap();
1970 assert_eq!(
1971 frag.message_type,
1972 Some(SipMessageType::Response {
1973 code: 200,
1974 reason: "OK".into()
1975 })
1976 );
1977 assert_eq!(frag.headers.len(), 1);
1978 assert_eq!(frag.body, b"v=0\r\n");
1979 }
1980
1981 #[test]
1982 fn sipfrag_request_start_line() {
1983 let frag = parse_sipfrag(b"INVITE sip:user@host SIP/2.0\r\nCSeq: 2 INVITE\r\n").unwrap();
1984 assert_eq!(
1985 frag.message_type,
1986 Some(SipMessageType::Request {
1987 method: "INVITE".into(),
1988 uri: "sip:user@host".into()
1989 })
1990 );
1991 assert_eq!(frag.headers.len(), 1);
1992 }
1993
1994 #[test]
1995 fn sipfrag_garbage_is_error() {
1996 assert!(parse_sipfrag(b"just some text without a colon").is_err());
1997 assert!(parse_sipfrag(b"").is_err());
1998 }
1999
2000 #[test]
2001 fn sipfrag_content_type_and_media_type() {
2002 let frag = parse_sipfrag(
2003 b"SIP/2.0 200 OK\r\nContent-Type: Application/SDP; charset=utf-8\r\n\r\nv=0",
2004 )
2005 .unwrap();
2006 assert_eq!(frag.content_type(), Some("Application/SDP; charset=utf-8"));
2007 assert_eq!(frag.media_type().as_deref(), Some("application/sdp"));
2008
2009 let compact = parse_sipfrag(b"SIP/2.0 200 OK\r\nc: text/plain\r\n\r\nhi").unwrap();
2010 assert_eq!(compact.content_type(), Some("text/plain"));
2011 }
2012
2013 #[test]
2014 fn sipfrag_malformed_start_line_with_colon_is_error() {
2015 assert!(parse_sipfrag(b"INVITE sip:host SIP/1.0\r\n").is_err());
2016 }
2017
2018 #[test]
2019 fn sipfrag_lf_only_status_line() {
2020 let frag = parse_sipfrag(b"SIP/2.0 200 OK\n").unwrap();
2021 assert!(matches!(
2022 frag.message_type,
2023 Some(SipMessageType::Response { code: 200, ref reason }) if reason == "OK"
2024 ));
2025 assert!(frag.headers.is_empty());
2026 assert!(frag.body.is_empty());
2027 }
2028
2029 #[test]
2030 fn sipfrag_from_mime_part() {
2031 let body = b"SIP/2.0 100 Trying\r\n";
2032 let msg = make_multipart_invite("frag-boundary", &[("message/sipfrag", body)]);
2033 let parsed = msg.parse().unwrap();
2034 let parts = parsed.body_as_parts();
2035 assert_eq!(parts[0].media_type().as_deref(), Some("message/sipfrag"));
2036
2037 let frag = parts[0].parse_sipfrag().unwrap();
2038 assert_eq!(
2039 frag.message_type,
2040 Some(SipMessageType::Response {
2041 code: 100,
2042 reason: "Trying".into()
2043 })
2044 );
2045 }
2046
2047 #[test]
2050 fn is_json_content_type_application_json() {
2051 assert!(is_json_content_type("application/json"));
2052 }
2053
2054 #[test]
2055 fn is_json_content_type_plus_json() {
2056 assert!(is_json_content_type(
2057 "application/emergencyCallData.AbandonedCall+json"
2058 ));
2059 }
2060
2061 #[test]
2062 fn is_json_content_type_with_params() {
2063 assert!(is_json_content_type("application/json; charset=utf-8"));
2064 }
2065
2066 #[test]
2067 fn is_json_content_type_case_insensitive() {
2068 assert!(is_json_content_type("Application/JSON"));
2069 }
2070
2071 #[test]
2072 fn is_json_content_type_not_text_plain() {
2073 assert!(!is_json_content_type("text/plain"));
2074 }
2075
2076 #[test]
2077 fn is_json_content_type_not_multipart() {
2078 assert!(!is_json_content_type("multipart/mixed;boundary=foo"));
2079 }
2080
2081 #[test]
2082 fn is_json_content_type_not_sdp() {
2083 assert!(!is_json_content_type("application/sdp"));
2084 }
2085
2086 #[test]
2089 fn unescape_json_basic_escapes() {
2090 let input = br#"{"key":"line1\r\nline2\ttab\"\\"}"#;
2091 let result = unescape_json_body(input);
2092 assert!(
2093 result.contains("line1\r\nline2\ttab\"\\"),
2094 "basic escapes not unescaped: {result:?}"
2095 );
2096 }
2097
2098 #[test]
2099 fn unescape_json_slash_and_control() {
2100 let input = br#"{"a":"\/path","b":"\b\f"}"#;
2101 let result = unescape_json_body(input);
2102 assert!(result.contains("/path"), "\\/ should become /");
2103 assert!(result.contains('\x08'), "\\b should become backspace");
2104 assert!(result.contains('\x0C'), "\\f should become form feed");
2105 }
2106
2107 #[test]
2108 fn unescape_json_unicode_basic() {
2109 let input = br#"{"x":"\u0041"}"#;
2111 let result = unescape_json_body(input);
2112 assert!(
2113 result.contains('A'),
2114 "\\u0041 should become 'A': {result:?}"
2115 );
2116 }
2117
2118 #[test]
2119 fn unescape_json_unicode_surrogate_pair() {
2120 let input = br#"{"emoji":"\uD83D\uDE00"}"#;
2122 let result = unescape_json_body(input);
2123 assert!(
2124 result.contains('\u{1F600}'),
2125 "surrogate pair should produce U+1F600: {result:?}"
2126 );
2127 }
2128
2129 #[test]
2130 fn unescape_json_passthrough_non_escape() {
2131 let input = b"no escapes here";
2132 let result = unescape_json_body(input);
2133 assert_eq!(result, "no escapes here");
2134 }
2135
2136 #[test]
2139 fn json_field_extract_string() {
2140 let body = br#"{"event":"AbandonedCall","id":"123"}"#;
2141 let mut content = Vec::new();
2142 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
2143 content.extend_from_slice(b"Call-ID: jf-test@host\r\n");
2144 content.extend_from_slice(b"Content-Type: application/json\r\n");
2145 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
2146 content.extend_from_slice(b"\r\n");
2147 content.extend_from_slice(body);
2148
2149 let msg = make_sip_message(&content);
2150 let parsed = msg.parse().unwrap();
2151
2152 assert_eq!(
2153 parsed.json_field("event"),
2154 Some("AbandonedCall".to_string())
2155 );
2156 assert_eq!(parsed.json_field("id"), Some("123".to_string()));
2157 }
2158
2159 #[test]
2160 fn json_field_missing_key() {
2161 let body = br#"{"event":"AbandonedCall"}"#;
2162 let mut content = Vec::new();
2163 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
2164 content.extend_from_slice(b"Call-ID: jf-miss@host\r\n");
2165 content.extend_from_slice(b"Content-Type: application/json\r\n");
2166 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
2167 content.extend_from_slice(b"\r\n");
2168 content.extend_from_slice(body);
2169
2170 let msg = make_sip_message(&content);
2171 let parsed = msg.parse().unwrap();
2172
2173 assert_eq!(parsed.json_field("nonexistent"), None);
2174 }
2175
2176 #[test]
2177 fn json_field_non_string_value() {
2178 let body = br#"{"count":42,"active":true}"#;
2179 let mut content = Vec::new();
2180 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
2181 content.extend_from_slice(b"Call-ID: jf-nonstr@host\r\n");
2182 content.extend_from_slice(b"Content-Type: application/json\r\n");
2183 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
2184 content.extend_from_slice(b"\r\n");
2185 content.extend_from_slice(body);
2186
2187 let msg = make_sip_message(&content);
2188 let parsed = msg.parse().unwrap();
2189
2190 assert_eq!(parsed.json_field("count"), None);
2191 assert_eq!(parsed.json_field("active"), None);
2192 }
2193
2194 #[test]
2195 fn json_field_non_json_content_type() {
2196 let body = br#"{"event":"AbandonedCall"}"#;
2197 let mut content = Vec::new();
2198 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
2199 content.extend_from_slice(b"Call-ID: jf-nonjson@host\r\n");
2200 content.extend_from_slice(b"Content-Type: text/plain\r\n");
2201 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
2202 content.extend_from_slice(b"\r\n");
2203 content.extend_from_slice(body);
2204
2205 let msg = make_sip_message(&content);
2206 let parsed = msg.parse().unwrap();
2207
2208 assert_eq!(parsed.json_field("event"), None);
2209 }
2210
2211 #[test]
2212 fn json_field_unescapes_value() {
2213 let body = br#"{"invite":"INVITE sip:host\r\nTo: <sip:host>\r\n"}"#;
2214 let mut content = Vec::new();
2215 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
2216 content.extend_from_slice(b"Call-ID: jf-unescape@host\r\n");
2217 content.extend_from_slice(b"Content-Type: application/json\r\n");
2218 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
2219 content.extend_from_slice(b"\r\n");
2220 content.extend_from_slice(body);
2221
2222 let msg = make_sip_message(&content);
2223 let parsed = msg.parse().unwrap();
2224
2225 let invite = parsed.json_field("invite").unwrap();
2226 assert!(
2227 invite.contains("INVITE sip:host\r\nTo: <sip:host>\r\n"),
2228 "json_field should return unescaped string: {invite:?}"
2229 );
2230 }
2231
2232 #[test]
2233 fn json_field_plus_json_content_type() {
2234 let body = br#"{"cancelTimestamp":"2025-12-14T05:35:03.269Z"}"#;
2235 let mut content = Vec::new();
2236 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
2237 content.extend_from_slice(b"Call-ID: jf-plus@host\r\n");
2238 content.extend_from_slice(
2239 b"Content-Type: application/emergencyCallData.AbandonedCall+json\r\n",
2240 );
2241 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
2242 content.extend_from_slice(b"\r\n");
2243 content.extend_from_slice(body);
2244
2245 let msg = make_sip_message(&content);
2246 let parsed = msg.parse().unwrap();
2247
2248 assert_eq!(
2249 parsed.json_field("cancelTimestamp"),
2250 Some("2025-12-14T05:35:03.269Z".to_string())
2251 );
2252 }
2253
2254 #[test]
2255 fn whitespace_only_returns_transport_noise() {
2256 use crate::frame::ParseError;
2257
2258 for content in [b"\n".as_slice(), b"\r\n", b"\n\n\n", b" \t\r\n"] {
2259 let msg = SipMessage {
2260 direction: Direction::Recv,
2261 transport: Transport::Tls,
2262 address: "[10.0.0.1]:5061".into(),
2263 timestamp: Timestamp::TimeOnly {
2264 hour: 0,
2265 min: 0,
2266 sec: 0,
2267 usec: 0,
2268 },
2269 content: content.to_vec(),
2270 frame_count: 1,
2271 };
2272 let err = msg.parse().unwrap_err();
2273 assert!(
2274 matches!(err, ParseError::TransportNoise { .. }),
2275 "whitespace-only content {:?} should produce TransportNoise, got: {err}",
2276 content,
2277 );
2278 }
2279 }
2280}