1use std::borrow::Cow;
2use std::sync::LazyLock;
3
4use memchr::memmem;
5
6use crate::frame::ParseError;
7use crate::message::MessageIterator;
8use crate::types::{
9 MimePart, ParseStats, ParsedSipMessage, SipMessage, SipMessageType, SkipTracking,
10 UnparsedRegion,
11};
12
13static CRLF: LazyLock<memmem::Finder<'static>> = LazyLock::new(|| memmem::Finder::new(b"\r\n"));
14static CRLFCRLF: LazyLock<memmem::Finder<'static>> =
15 LazyLock::new(|| memmem::Finder::new(b"\r\n\r\n"));
16
17impl SipMessage {
18 pub fn parse(&self) -> Result<ParsedSipMessage, ParseError> {
21 parse_sip_message(self)
22 }
23}
24
25pub struct ParsedMessageIterator<R> {
46 inner: MessageIterator<R>,
47}
48
49impl<R: std::io::Read> ParsedMessageIterator<R> {
50 pub fn new(reader: R) -> Self {
52 ParsedMessageIterator {
53 inner: MessageIterator::new(reader),
54 }
55 }
56
57 pub fn capture_skipped(mut self, enable: bool) -> Self {
59 self.inner = self.inner.capture_skipped(enable);
60 self
61 }
62
63 pub fn skip_tracking(mut self, tracking: SkipTracking) -> Self {
65 self.inner = self.inner.skip_tracking(tracking);
66 self
67 }
68
69 pub fn parse_stats(&self) -> &ParseStats {
71 self.inner.parse_stats()
72 }
73
74 pub fn parse_stats_mut(&mut self) -> &mut ParseStats {
76 self.inner.parse_stats_mut()
77 }
78
79 pub fn drain_unparsed(&mut self) -> Vec<UnparsedRegion> {
81 self.inner.drain_unparsed()
82 }
83}
84
85impl<R: std::io::Read> Iterator for ParsedMessageIterator<R> {
86 type Item = Result<ParsedSipMessage, ParseError>;
87
88 fn next(&mut self) -> Option<Self::Item> {
89 let msg = match self.inner.next()? {
90 Ok(m) => m,
91 Err(e) => return Some(Err(e)),
92 };
93 Some(msg.parse())
94 }
95}
96
97fn content_preview(content: &[u8], max_len: usize) -> String {
98 use std::fmt::Write;
99 let len = content.len().min(max_len);
100 let s = String::from_utf8_lossy(&content[..len]);
101 let mut out = String::with_capacity(s.len());
102 for c in s.chars() {
103 match c {
104 '\r' => out.push_str("\\r"),
105 '\n' => out.push_str("\\n"),
106 '\t' => out.push_str("\\t"),
107 '\0' => out.push_str("\\0"),
108 c if c.is_control() => {
109 let _ = write!(out, "\\x{:02x}", c as u32);
110 }
111 c => out.push(c),
112 }
113 }
114 if content.len() > max_len {
115 out.push_str("...");
116 }
117 out
118}
119
120fn parse_sip_message(msg: &SipMessage) -> Result<ParsedSipMessage, ParseError> {
121 let content = &msg.content;
122
123 if content
124 .iter()
125 .all(|&b| matches!(b, b'\r' | b'\n' | b' ' | b'\t'))
126 {
127 return Err(ParseError::TransportNoise {
128 bytes: content.len(),
129 transport: msg.transport,
130 address: msg.address.clone(),
131 });
132 }
133
134 parse_sip_content(msg, content).map_err(|e| {
135 let reason = match e {
136 ParseError::InvalidMessage(reason) => reason,
137 other => return other,
138 };
139 let preview = content_preview(content, 200);
140 ParseError::InvalidMessage(format!(
141 "{} {}/{} at {} ({} frames, {} bytes): {reason}\n {preview}",
142 msg.direction,
143 msg.transport,
144 msg.address,
145 msg.timestamp,
146 msg.frame_count,
147 content.len(),
148 ))
149 })
150}
151
152fn parse_sip_content(msg: &SipMessage, content: &[u8]) -> Result<ParsedSipMessage, ParseError> {
153 let first_line_end = CRLF
155 .find(content)
156 .ok_or_else(|| ParseError::InvalidMessage("no CRLF found".into()))?;
157 let first_line = &content[..first_line_end];
158
159 let message_type = parse_first_line(first_line)?;
160
161 let header_end = CRLFCRLF.find(content);
163 let (headers, body) = match header_end {
164 Some(pos) if pos > first_line_end + 1 => {
165 let header_bytes = &content[first_line_end + 2..pos];
166 let body = &content[pos + 4..];
167 (header_bytes, body)
168 }
169 Some(pos) => {
170 let body = &content[pos + 4..];
171 (&[][..], body)
172 }
173 None => {
174 let header_bytes = &content[first_line_end + 2..];
176 (header_bytes, &[][..])
177 }
178 };
179
180 let headers = parse_headers(headers);
181
182 Ok(ParsedSipMessage {
183 direction: msg.direction,
184 transport: msg.transport,
185 address: msg.address.clone(),
186 timestamp: msg.timestamp,
187 message_type,
188 headers,
189 body: body.to_vec(),
190 frame_count: msg.frame_count,
191 })
192}
193
194fn parse_first_line(line: &[u8]) -> Result<SipMessageType, ParseError> {
195 if line.starts_with(b"SIP/2.0 ") {
196 return parse_status_line(line);
197 }
198 parse_request_line(line)
199}
200
201fn parse_status_line(line: &[u8]) -> Result<SipMessageType, ParseError> {
202 let after_version = &line[8..]; let space = memchr::memchr(b' ', after_version)
206 .ok_or_else(|| ParseError::InvalidMessage("no space after status code".into()))?;
207 let code_bytes = &after_version[..space];
208 let code: u16 = std::str::from_utf8(code_bytes)
209 .map_err(|_| ParseError::InvalidMessage("non-UTF-8 status code".into()))?
210 .parse()
211 .map_err(|_| ParseError::InvalidMessage("invalid status code".into()))?;
212
213 let reason = &after_version[space + 1..];
214 let reason = bytes_to_string(reason);
215
216 Ok(SipMessageType::Response { code, reason })
217}
218
219fn is_sip_token(b: &[u8]) -> bool {
220 !b.is_empty()
221 && b.iter()
222 .all(|&c| c.is_ascii_alphanumeric() || b"-._!%*+'~".contains(&c))
223}
224
225fn parse_request_line(line: &[u8]) -> Result<SipMessageType, ParseError> {
226 let first_space = memchr::memchr(b' ', line)
228 .ok_or_else(|| ParseError::InvalidMessage("no space in request line".into()))?;
229 let method = &line[..first_space];
230
231 if !is_sip_token(method) {
232 return Err(ParseError::InvalidMessage(format!(
233 "invalid SIP method: {:?}",
234 String::from_utf8_lossy(method)
235 )));
236 }
237 let rest = &line[first_space + 1..];
238
239 let last_space = memchr::memrchr(b' ', rest)
240 .ok_or_else(|| ParseError::InvalidMessage("no SIP version in request line".into()))?;
241 let version = &rest[last_space + 1..];
242 if version != b"SIP/2.0" {
243 return Err(ParseError::InvalidMessage(format!(
244 "expected SIP/2.0, got {:?}",
245 String::from_utf8_lossy(version)
246 )));
247 }
248 let uri = &rest[..last_space];
249
250 let method = bytes_to_string(method);
251 let uri = bytes_to_string(uri);
252
253 Ok(SipMessageType::Request { method, uri })
254}
255
256fn bytes_to_string(b: &[u8]) -> String {
257 if b.is_ascii() {
258 unsafe { String::from_utf8_unchecked(b.to_vec()) }
260 } else {
261 String::from_utf8_lossy(b).into_owned()
262 }
263}
264
265fn parse_headers(data: &[u8]) -> Vec<(String, String)> {
266 let mut headers = Vec::new();
267 if data.is_empty() {
268 return headers;
269 }
270
271 let mut pos = 0;
272 while pos < data.len() {
273 let line_end = CRLF.find(&data[pos..]).unwrap_or(data.len() - pos);
274 let mut line = &data[pos..pos + line_end];
275 pos += line_end + 2; while pos < data.len() && (data[pos] == b' ' || data[pos] == b'\t') {
279 let next_end = CRLF.find(&data[pos..]).unwrap_or(data.len() - pos);
280 line = &data[line.as_ptr() as usize - data.as_ptr() as usize..pos + next_end];
282 pos += next_end + 2;
283 }
284
285 if line.is_empty() {
286 continue;
287 }
288
289 if let Some(colon) = memchr::memchr(b':', line) {
290 let name = &line[..colon];
291 let value = if colon + 1 < line.len() {
292 trim_header_value(&line[colon + 1..])
293 } else {
294 &[]
295 };
296 headers.push((bytes_to_string(name), bytes_to_string(value)));
297 }
298 }
299
300 headers
301}
302
303fn trim_header_value(b: &[u8]) -> &[u8] {
304 let start = b
305 .iter()
306 .position(|&c| c != b' ' && c != b'\t')
307 .unwrap_or(b.len());
308 &b[start..]
309}
310
311impl ParsedSipMessage {
312 pub fn is_multipart(&self) -> bool {
314 self.content_type()
315 .map(|ct| ct.to_ascii_lowercase().starts_with("multipart/"))
316 .unwrap_or(false)
317 }
318
319 pub fn multipart_boundary(&self) -> Option<&str> {
321 let ct = self.content_type()?;
322 extract_boundary(ct)
323 }
324
325 pub fn body_parts(&self) -> Option<Vec<MimePart>> {
328 let boundary = self.multipart_boundary()?;
329 Some(parse_multipart_body(&self.body, boundary))
330 }
331
332 pub fn body_text(&self) -> Cow<'_, str> {
337 if let Some(ct) = self.content_type() {
338 if is_json_content_type(ct) {
339 return Cow::Owned(unescape_json_body(&self.body));
340 }
341 }
342 self.body_data()
343 }
344
345 pub fn json_field(&self, key: &str) -> Option<String> {
349 let ct = self.content_type()?;
350 if !is_json_content_type(ct) {
351 return None;
352 }
353 let value: serde_json::Value = serde_json::from_slice(&self.body).ok()?;
354 let obj = value.as_object()?;
355 obj.get(key)?.as_str().map(|s| s.to_string())
356 }
357}
358
359fn is_json_content_type(ct: &str) -> bool {
360 let media_type = ct.split(';').next().unwrap_or("").trim();
361 let lower = media_type.to_ascii_lowercase();
362 lower == "application/json" || (lower.starts_with("application/") && lower.ends_with("+json"))
363}
364
365fn unescape_json_body(input: &[u8]) -> String {
366 let s = String::from_utf8_lossy(input);
367 let mut out = String::with_capacity(s.len());
368 let mut chars = s.chars();
369
370 while let Some(c) = chars.next() {
371 if c != '\\' {
372 out.push(c);
373 continue;
374 }
375 match chars.next() {
376 Some('"') => out.push('"'),
377 Some('\\') => out.push('\\'),
378 Some('/') => out.push('/'),
379 Some('b') => out.push('\x08'),
380 Some('f') => out.push('\x0C'),
381 Some('n') => out.push('\n'),
382 Some('r') => out.push('\r'),
383 Some('t') => out.push('\t'),
384 Some('u') => unescape_unicode(&mut chars, &mut out),
385 Some(other) => {
386 out.push('\\');
387 out.push(other);
388 }
389 None => out.push('\\'),
390 }
391 }
392 out
393}
394
395fn unescape_unicode(chars: &mut std::str::Chars<'_>, out: &mut String) {
396 let hex: String = chars.by_ref().take(4).collect();
397 let Some(code_point) = parse_hex4(&hex) else {
398 out.push_str("\\u");
399 out.push_str(&hex);
400 return;
401 };
402
403 if (0xD800..=0xDBFF).contains(&code_point) {
404 let mut peek = chars.clone();
405 if peek.next() == Some('\\') && peek.next() == Some('u') {
406 let hex2: String = peek.by_ref().take(4).collect();
407 if let Some(low) = parse_hex4(&hex2) {
408 if (0xDC00..=0xDFFF).contains(&low) {
409 let combined =
410 0x10000 + ((code_point as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
411 if let Some(ch) = char::from_u32(combined) {
412 out.push(ch);
413 *chars = peek;
414 return;
415 }
416 }
417 }
418 }
419 out.push_str("\\u");
420 out.push_str(&hex);
421 } else if let Some(ch) = char::from_u32(code_point as u32) {
422 out.push(ch);
423 } else {
424 out.push_str("\\u");
425 out.push_str(&hex);
426 }
427}
428
429fn parse_hex4(hex: &str) -> Option<u16> {
430 if hex.len() == 4 {
431 u16::from_str_radix(hex, 16).ok()
432 } else {
433 None
434 }
435}
436
437fn extract_boundary(content_type: &str) -> Option<&str> {
438 let lower = content_type.to_ascii_lowercase();
439 let idx = lower.find("boundary=")?;
440 let after = &content_type[idx + 9..];
441
442 if let Some(after_quote) = after.strip_prefix('"') {
443 let end_quote = after_quote.find('"')?;
444 Some(&after_quote[..end_quote])
445 } else {
446 let end = after.find(';').unwrap_or(after.len());
447 let boundary = after[..end].trim();
448 if boundary.is_empty() {
449 None
450 } else {
451 Some(boundary)
452 }
453 }
454}
455
456fn parse_multipart_body(body: &[u8], boundary: &str) -> Vec<MimePart> {
457 let open_delim = format!("--{boundary}");
458 let open_bytes = open_delim.as_bytes();
459
460 let mut parts = Vec::new();
461
462 let mut pos = match memmem::find(body, open_bytes) {
464 Some(p) => p + open_bytes.len(),
465 None => return parts,
466 };
467
468 if body[pos..].starts_with(b"--") {
470 return parts;
471 }
472
473 if body[pos..].starts_with(b"\r\n") {
475 pos += 2;
476 }
477
478 while let Some(next) = memmem::find(&body[pos..], open_bytes) {
479 let mut end = pos + next;
481 if end >= 2 && body[end - 2] == b'\r' && body[end - 1] == b'\n' {
482 end -= 2;
483 }
484
485 parts.push(parse_mime_part(&body[pos..end]));
486
487 pos = pos + next + open_bytes.len();
489
490 if body[pos..].starts_with(b"--") {
492 break;
493 }
494
495 if body[pos..].starts_with(b"\r\n") {
497 pos += 2;
498 }
499 }
500
501 parts
502}
503
504fn parse_mime_part(data: &[u8]) -> MimePart {
505 match memmem::find(data, b"\r\n\r\n") {
506 Some(pos) => {
507 let header_bytes = &data[..pos];
508 let body = &data[pos + 4..];
509 let headers = parse_headers(header_bytes);
510 MimePart {
511 headers,
512 body: body.to_vec(),
513 }
514 }
515 None => {
516 let first_line_end = memmem::find(data, b"\r\n").unwrap_or(data.len());
519 if memchr::memchr(b':', &data[..first_line_end]).is_some() {
520 let headers = parse_headers(data);
521 MimePart {
522 headers,
523 body: Vec::new(),
524 }
525 } else {
526 MimePart {
527 headers: Vec::new(),
528 body: data.to_vec(),
529 }
530 }
531 }
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use crate::types::{Direction, SipMessage, Timestamp, Transport};
539
540 fn make_sip_message(content: &[u8]) -> SipMessage {
541 SipMessage {
542 direction: Direction::Recv,
543 transport: Transport::Udp,
544 address: "10.0.0.1:5060".into(),
545 timestamp: Timestamp::TimeOnly {
546 hour: 12,
547 min: 0,
548 sec: 0,
549 usec: 0,
550 },
551 content: content.to_vec(),
552 frame_count: 1,
553 }
554 }
555
556 #[test]
557 fn parse_stats_delegates() {
558 let content =
559 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: stats-test\r\nContent-Length: 0\r\n\r\n";
560 let header = format!(
561 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
562 content.len()
563 );
564 let mut data = header.into_bytes();
565 data.extend_from_slice(content);
566 data.extend_from_slice(b"\x0B\n");
567
568 let mut iter = ParsedMessageIterator::new(&data[..]);
569 let parsed: Vec<_> = iter.by_ref().collect::<Result<Vec<_>, _>>().unwrap();
570 assert_eq!(parsed.len(), 1);
571 let stats = iter.parse_stats();
572 assert_eq!(stats.bytes_read, data.len() as u64);
573 assert_eq!(stats.bytes_skipped, 0);
574 }
575
576 #[test]
577 fn parse_options_request() {
578 let content = b"OPTIONS sip:user@host SIP/2.0\r\n\
579 Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-1\r\n\
580 From: <sip:user@host>;tag=abc\r\n\
581 To: <sip:user@host>\r\n\
582 Call-ID: test-call-id@host\r\n\
583 CSeq: 1 OPTIONS\r\n\
584 Content-Length: 0\r\n\
585 \r\n";
586 let msg = make_sip_message(content);
587 let parsed = msg.parse().unwrap();
588
589 assert_eq!(
590 parsed.message_type,
591 SipMessageType::Request {
592 method: "OPTIONS".into(),
593 uri: "sip:user@host".into()
594 }
595 );
596 assert_eq!(parsed.call_id(), Some("test-call-id@host"));
597 assert_eq!(parsed.cseq(), Some("1 OPTIONS"));
598 assert_eq!(parsed.content_length(), Some(0));
599 assert_eq!(parsed.method(), Some("OPTIONS"));
600 assert!(parsed.body.is_empty());
601 }
602
603 #[test]
604 fn parse_200_ok_response() {
605 let content = b"SIP/2.0 200 OK\r\n\
606 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
607 Call-ID: resp-id@host\r\n\
608 CSeq: 1 INVITE\r\n\
609 Content-Length: 0\r\n\
610 \r\n";
611 let msg = make_sip_message(content);
612 let parsed = msg.parse().unwrap();
613
614 assert_eq!(
615 parsed.message_type,
616 SipMessageType::Response {
617 code: 200,
618 reason: "OK".into()
619 }
620 );
621 assert_eq!(parsed.method(), Some("INVITE"));
622 }
623
624 #[test]
625 fn parse_100_trying() {
626 let content = b"SIP/2.0 100 Trying\r\n\
627 Via: SIP/2.0/TCP 10.0.0.1:5060\r\n\
628 Call-ID: trying-id\r\n\
629 CSeq: 42 INVITE\r\n\
630 Content-Length: 0\r\n\
631 \r\n";
632 let msg = make_sip_message(content);
633 let parsed = msg.parse().unwrap();
634
635 assert_eq!(
636 parsed.message_type,
637 SipMessageType::Response {
638 code: 100,
639 reason: "Trying".into()
640 }
641 );
642 assert_eq!(parsed.method(), Some("INVITE"));
643 }
644
645 #[test]
646 fn parse_invite_with_sdp_body() {
647 let body = b"v=0\r\no=- 123 456 IN IP4 10.0.0.1\r\ns=-\r\n";
648 let mut content = Vec::new();
649 content.extend_from_slice(b"INVITE sip:user@host SIP/2.0\r\n");
650 content.extend_from_slice(b"Call-ID: invite-body@host\r\n");
651 content.extend_from_slice(b"CSeq: 1 INVITE\r\n");
652 content.extend_from_slice(b"Content-Type: application/sdp\r\n");
653 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
654 content.extend_from_slice(b"\r\n");
655 content.extend_from_slice(body);
656
657 let msg = make_sip_message(&content);
658 let parsed = msg.parse().unwrap();
659
660 assert_eq!(parsed.method(), Some("INVITE"));
661 assert_eq!(parsed.content_type(), Some("application/sdp"));
662 assert_eq!(parsed.content_length(), Some(body.len()));
663 assert_eq!(parsed.body, body);
664 }
665
666 #[test]
667 fn parse_notify_with_json_body() {
668 let body = br#"{"event":"AbandonedCall","id":"123"}"#;
669 let mut content = Vec::new();
670 content.extend_from_slice(b"NOTIFY sip:user@host SIP/2.0\r\n");
671 content.extend_from_slice(b"Call-ID: notify-json@host\r\n");
672 content.extend_from_slice(b"CSeq: 1 NOTIFY\r\n");
673 content.extend_from_slice(b"Content-Type: application/json\r\n");
674 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
675 content.extend_from_slice(b"\r\n");
676 content.extend_from_slice(body);
677
678 let msg = make_sip_message(&content);
679 let parsed = msg.parse().unwrap();
680
681 assert_eq!(parsed.method(), Some("NOTIFY"));
682 assert_eq!(parsed.content_type(), Some("application/json"));
683 assert_eq!(parsed.body, body);
684 }
685
686 #[test]
687 fn compact_headers() {
688 let content = b"NOTIFY sip:user@host SIP/2.0\r\n\
689 i: compact-call-id\r\n\
690 l: 0\r\n\
691 c: text/plain\r\n\
692 \r\n";
693 let msg = make_sip_message(content);
694 let parsed = msg.parse().unwrap();
695
696 assert_eq!(parsed.call_id(), Some("compact-call-id"));
697 assert_eq!(parsed.content_length(), Some(0));
698 assert_eq!(parsed.content_type(), Some("text/plain"));
699 }
700
701 #[test]
702 fn header_folding() {
703 let content = b"OPTIONS sip:host SIP/2.0\r\n\
704 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
705 Subject: this is a long\r\n \
706 folded header value\r\n\
707 Call-ID: fold-test\r\n\
708 Content-Length: 0\r\n\
709 \r\n";
710 let msg = make_sip_message(content);
711 let parsed = msg.parse().unwrap();
712
713 let subject = parsed
714 .headers
715 .iter()
716 .find(|(k, _)| k == "Subject")
717 .map(|(_, v)| v.as_str());
718 assert!(
719 subject.unwrap().contains("folded header value"),
720 "folded header should be reconstructed: {:?}",
721 subject
722 );
723 assert_eq!(parsed.call_id(), Some("fold-test"));
724 }
725
726 #[test]
727 fn no_body() {
728 let content = b"OPTIONS sip:host SIP/2.0\r\n\
729 Call-ID: nobody\r\n\
730 Content-Length: 0\r\n\
731 \r\n";
732 let msg = make_sip_message(content);
733 let parsed = msg.parse().unwrap();
734 assert!(parsed.body.is_empty());
735 }
736
737 #[test]
738 fn no_blank_line_no_body() {
739 let content = b"OPTIONS sip:host SIP/2.0\r\n\
741 Call-ID: no-blank\r\n\
742 Content-Length: 0";
743 let msg = make_sip_message(content);
744 let parsed = msg.parse().unwrap();
745 assert!(parsed.body.is_empty());
746 assert_eq!(parsed.call_id(), Some("no-blank"));
747 }
748
749 #[test]
750 fn preserves_metadata() {
751 let content = b"REGISTER sip:host SIP/2.0\r\n\
752 Call-ID: meta-test\r\n\
753 \r\n";
754 let msg = SipMessage {
755 direction: Direction::Sent,
756 transport: Transport::Tls,
757 address: "[2001:db8::1]:5061".into(),
758 timestamp: Timestamp::DateTime {
759 year: 2026,
760 month: 2,
761 day: 12,
762 hour: 10,
763 min: 30,
764 sec: 0,
765 usec: 123456,
766 },
767 content: content.to_vec(),
768 frame_count: 3,
769 };
770 let parsed = msg.parse().unwrap();
771
772 assert_eq!(parsed.direction, Direction::Sent);
773 assert_eq!(parsed.transport, Transport::Tls);
774 assert_eq!(parsed.address, "[2001:db8::1]:5061");
775 assert_eq!(parsed.frame_count, 3);
776 assert_eq!(
777 parsed.timestamp,
778 Timestamp::DateTime {
779 year: 2026,
780 month: 2,
781 day: 12,
782 hour: 10,
783 min: 30,
784 sec: 0,
785 usec: 123456,
786 }
787 );
788 }
789
790 #[test]
791 fn multiple_same_name_headers() {
792 let content = b"INVITE sip:host SIP/2.0\r\n\
793 Via: SIP/2.0/UDP proxy1:5060\r\n\
794 Via: SIP/2.0/UDP proxy2:5060\r\n\
795 Record-Route: <sip:proxy1>\r\n\
796 Record-Route: <sip:proxy2>\r\n\
797 Call-ID: multi-hdr\r\n\
798 Content-Length: 0\r\n\
799 \r\n";
800 let msg = make_sip_message(content);
801 let parsed = msg.parse().unwrap();
802
803 let via_count = parsed.headers.iter().filter(|(k, _)| k == "Via").count();
804 assert_eq!(via_count, 2);
805
806 let rr_count = parsed
807 .headers
808 .iter()
809 .filter(|(k, _)| k == "Record-Route")
810 .count();
811 assert_eq!(rr_count, 2);
812 }
813
814 #[test]
815 fn header_ordering_preserved() {
816 let content = b"OPTIONS sip:host SIP/2.0\r\n\
817 Via: v1\r\n\
818 From: f1\r\n\
819 To: t1\r\n\
820 Call-ID: order-test\r\n\
821 CSeq: 1 OPTIONS\r\n\
822 \r\n";
823 let msg = make_sip_message(content);
824 let parsed = msg.parse().unwrap();
825
826 let names: Vec<&str> = parsed.headers.iter().map(|(k, _)| k.as_str()).collect();
827 assert_eq!(names, vec!["Via", "From", "To", "Call-ID", "CSeq"]);
828 }
829
830 #[test]
831 fn status_line_with_long_reason() {
832 let content = b"SIP/2.0 486 Busy Here\r\n\
833 Call-ID: busy\r\n\
834 \r\n";
835 let msg = make_sip_message(content);
836 let parsed = msg.parse().unwrap();
837
838 assert_eq!(
839 parsed.message_type,
840 SipMessageType::Response {
841 code: 486,
842 reason: "Busy Here".into()
843 }
844 );
845 }
846
847 #[test]
848 fn request_with_complex_uri() {
849 let content = b"INVITE sip:+15551234567@gateway.example.com;transport=tcp SIP/2.0\r\n\
850 Call-ID: complex-uri\r\n\
851 \r\n";
852 let msg = make_sip_message(content);
853 let parsed = msg.parse().unwrap();
854
855 assert_eq!(
856 parsed.message_type,
857 SipMessageType::Request {
858 method: "INVITE".into(),
859 uri: "sip:+15551234567@gateway.example.com;transport=tcp".into()
860 }
861 );
862 }
863
864 #[test]
865 fn binary_body() {
866 let body: Vec<u8> = (0..256).map(|i| i as u8).collect();
867 let mut content = Vec::new();
868 content.extend_from_slice(b"MESSAGE sip:host SIP/2.0\r\n");
869 content.extend_from_slice(b"Call-ID: binary-body\r\n");
870 content.extend_from_slice(b"Content-Type: application/octet-stream\r\n");
871 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
872 content.extend_from_slice(b"\r\n");
873 content.extend_from_slice(&body);
874
875 let msg = make_sip_message(&content);
876 let parsed = msg.parse().unwrap();
877
878 assert_eq!(parsed.body, body);
879 }
880
881 #[test]
882 fn error_no_crlf() {
883 let content = b"garbage without any crlf";
884 let msg = make_sip_message(content);
885 let result = msg.parse();
886 assert!(result.is_err());
887 }
888
889 #[test]
890 fn error_no_space_in_request_line() {
891 let content = b"INVALID\r\n\r\n";
892 let msg = make_sip_message(content);
893 let result = msg.parse();
894 assert!(result.is_err());
895 }
896
897 #[test]
898 fn parse_request_rejects_xml_method() {
899 let content =
900 b"</confInfo:conference-info>NOTIFY sip:user@host SIP/2.0\r\nContent-Length: 0\r\n\r\n";
901 let msg = make_sip_message(content);
902 assert!(msg.parse().is_err(), "should reject XML-prefixed method");
903 }
904
905 #[test]
906 fn parse_request_rejects_method_with_angle_brackets() {
907 let content = b"<xml>BYE sip:host SIP/2.0\r\n\r\n";
908 let msg = make_sip_message(content);
909 assert!(msg.parse().is_err());
910 }
911
912 #[test]
913 fn parse_request_accepts_extension_method() {
914 let content = b"CUSTOM-METHOD sip:host SIP/2.0\r\nContent-Length: 0\r\n\r\n";
915 let msg = make_sip_message(content);
916 let parsed = msg.parse().unwrap();
917 assert_eq!(
918 parsed.message_type,
919 SipMessageType::Request {
920 method: "CUSTOM-METHOD".into(),
921 uri: "sip:host".into()
922 }
923 );
924 }
925
926 #[test]
927 fn header_value_with_colon() {
928 let content = b"INVITE sip:host SIP/2.0\r\n\
930 Contact: <sip:user@10.0.0.1:5060;transport=tcp>\r\n\
931 Call-ID: colon-val\r\n\
932 \r\n";
933 let msg = make_sip_message(content);
934 let parsed = msg.parse().unwrap();
935
936 let contact = parsed
937 .headers
938 .iter()
939 .find(|(k, _)| k == "Contact")
940 .map(|(_, v)| v.as_str());
941 assert_eq!(contact, Some("<sip:user@10.0.0.1:5060;transport=tcp>"));
942 }
943
944 #[test]
945 fn whitespace_around_header_value() {
946 let content = b"OPTIONS sip:host SIP/2.0\r\n\
947 Call-ID: spaces-around \r\n\
948 \r\n";
949 let msg = make_sip_message(content);
950 let parsed = msg.parse().unwrap();
951
952 assert_eq!(parsed.call_id(), Some("spaces-around "));
954 }
955
956 #[test]
957 fn parsed_message_iterator() {
958 let content =
959 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: iter-test\r\nContent-Length: 0\r\n\r\n";
960 let header = format!(
961 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
962 content.len()
963 );
964 let mut data = header.into_bytes();
965 data.extend_from_slice(content);
966 data.extend_from_slice(b"\x0B\n");
967
968 let parsed: Vec<ParsedSipMessage> = ParsedMessageIterator::new(&data[..])
969 .collect::<Result<Vec<_>, _>>()
970 .unwrap();
971
972 assert_eq!(parsed.len(), 1);
973 assert_eq!(parsed[0].call_id(), Some("iter-test"));
974 assert_eq!(parsed[0].method(), Some("OPTIONS"));
975 }
976
977 fn make_multipart_invite(boundary: &str, parts: &[(&str, &[u8])]) -> SipMessage {
980 let mut body = Vec::new();
981 for (ct, content) in parts {
982 body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
983 body.extend_from_slice(format!("Content-Type: {ct}\r\n").as_bytes());
984 body.extend_from_slice(b"\r\n");
985 body.extend_from_slice(content);
986 body.extend_from_slice(b"\r\n");
987 }
988 body.extend_from_slice(format!("--{boundary}--").as_bytes());
989
990 let mut content = Vec::new();
991 content.extend_from_slice(b"INVITE sip:urn:service:sos@esrp.example.com SIP/2.0\r\n");
992 content.extend_from_slice(b"Call-ID: multipart-test@host\r\n");
993 content.extend_from_slice(b"CSeq: 1 INVITE\r\n");
994 content.extend_from_slice(
995 format!("Content-Type: multipart/mixed;boundary={boundary}\r\n").as_bytes(),
996 );
997 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
998 content.extend_from_slice(b"\r\n");
999 content.extend_from_slice(&body);
1000
1001 make_sip_message(&content)
1002 }
1003
1004 #[test]
1005 fn multipart_sdp_and_pidf() {
1006 let sdp = b"v=0\r\no=- 123 456 IN IP4 10.0.0.1\r\ns=-\r\n";
1007 let pidf = b"<?xml version=\"1.0\"?>\r\n<presence xmlns=\"urn:ietf:params:xml:ns:pidf\"/>";
1008 let msg = make_multipart_invite(
1009 "unique-boundary-1",
1010 &[("application/sdp", sdp), ("application/pidf+xml", pidf)],
1011 );
1012 let parsed = msg.parse().unwrap();
1013
1014 assert!(parsed.is_multipart());
1015 assert_eq!(parsed.multipart_boundary(), Some("unique-boundary-1"));
1016
1017 let parts = parsed.body_parts().unwrap();
1018 assert_eq!(parts.len(), 2);
1019
1020 assert_eq!(parts[0].content_type(), Some("application/sdp"));
1021 assert_eq!(parts[0].body, sdp);
1022
1023 assert_eq!(parts[1].content_type(), Some("application/pidf+xml"));
1024 assert_eq!(parts[1].body, pidf);
1025 }
1026
1027 #[test]
1028 fn multipart_sdp_and_eido() {
1029 let sdp = b"v=0\r\no=- 1 1 IN IP4 10.0.0.1\r\ns=-\r\n\
1030 c=IN IP4 10.0.0.1\r\nt=0 0\r\nm=audio 8000 RTP/AVP 0\r\n";
1031 let eido = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\
1032 <eido:EmergencyCallData xmlns:eido=\"urn:nena:xml:ns:EmergencyCallData\">\r\n\
1033 <eido:IncidentId>INC-2026-001</eido:IncidentId>\r\n\
1034 </eido:EmergencyCallData>";
1035 let msg = make_multipart_invite(
1036 "ng911-boundary",
1037 &[
1038 ("application/sdp", sdp),
1039 ("application/emergencyCallData.eido+xml", eido),
1040 ],
1041 );
1042 let parsed = msg.parse().unwrap();
1043 let parts = parsed.body_parts().unwrap();
1044 assert_eq!(parts.len(), 2);
1045
1046 let sdp_part = parts
1047 .iter()
1048 .find(|p| p.content_type() == Some("application/sdp"));
1049 assert!(sdp_part.is_some());
1050 assert_eq!(sdp_part.unwrap().body, sdp);
1051
1052 let eido_part = parts
1053 .iter()
1054 .find(|p| p.content_type().is_some_and(|ct| ct.contains("eido")));
1055 assert!(eido_part.is_some());
1056 assert_eq!(eido_part.unwrap().body, eido);
1057 }
1058
1059 #[test]
1060 fn multipart_three_parts_sdp_pidf_eido() {
1061 let sdp = b"v=0\r\ns=-\r\n";
1062 let pidf = b"<presence/>";
1063 let eido = b"<EmergencyCallData/>";
1064 let msg = make_multipart_invite(
1065 "tri-part",
1066 &[
1067 ("application/sdp", sdp),
1068 ("application/pidf+xml", pidf),
1069 ("application/emergencyCallData.eido+xml", eido),
1070 ],
1071 );
1072 let parsed = msg.parse().unwrap();
1073 let parts = parsed.body_parts().unwrap();
1074 assert_eq!(parts.len(), 3);
1075 assert_eq!(parts[0].content_type(), Some("application/sdp"));
1076 assert_eq!(parts[1].content_type(), Some("application/pidf+xml"));
1077 assert_eq!(
1078 parts[2].content_type(),
1079 Some("application/emergencyCallData.eido+xml")
1080 );
1081 }
1082
1083 #[test]
1084 fn multipart_quoted_boundary() {
1085 let sdp = b"v=0\r\n";
1086 let pidf = b"<presence/>";
1087
1088 let mut body = Vec::new();
1089 body.extend_from_slice(b"--quoted-boundary\r\n");
1090 body.extend_from_slice(b"Content-Type: application/sdp\r\n\r\n");
1091 body.extend_from_slice(sdp);
1092 body.extend_from_slice(b"\r\n--quoted-boundary\r\n");
1093 body.extend_from_slice(b"Content-Type: application/pidf+xml\r\n\r\n");
1094 body.extend_from_slice(pidf);
1095 body.extend_from_slice(b"\r\n--quoted-boundary--");
1096
1097 let mut content = Vec::new();
1098 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1099 content.extend_from_slice(b"Call-ID: quoted-bnd@host\r\n");
1100 content
1101 .extend_from_slice(b"Content-Type: multipart/mixed; boundary=\"quoted-boundary\"\r\n");
1102 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1103 content.extend_from_slice(b"\r\n");
1104 content.extend_from_slice(&body);
1105
1106 let msg = make_sip_message(&content);
1107 let parsed = msg.parse().unwrap();
1108
1109 assert_eq!(parsed.multipart_boundary(), Some("quoted-boundary"));
1110 let parts = parsed.body_parts().unwrap();
1111 assert_eq!(parts.len(), 2);
1112 assert_eq!(parts[0].body, sdp);
1113 assert_eq!(parts[1].body, pidf);
1114 }
1115
1116 #[test]
1117 fn multipart_with_preamble() {
1118 let sdp = b"v=0\r\n";
1119
1120 let mut body = Vec::new();
1121 body.extend_from_slice(b"This is the preamble. It should be ignored.\r\n");
1122 body.extend_from_slice(b"--boundary-pre\r\n");
1123 body.extend_from_slice(b"Content-Type: application/sdp\r\n\r\n");
1124 body.extend_from_slice(sdp);
1125 body.extend_from_slice(b"\r\n--boundary-pre--");
1126
1127 let mut content = Vec::new();
1128 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1129 content.extend_from_slice(b"Call-ID: preamble@host\r\n");
1130 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=boundary-pre\r\n");
1131 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1132 content.extend_from_slice(b"\r\n");
1133 content.extend_from_slice(&body);
1134
1135 let msg = make_sip_message(&content);
1136 let parsed = msg.parse().unwrap();
1137 let parts = parsed.body_parts().unwrap();
1138 assert_eq!(parts.len(), 1);
1139 assert_eq!(parts[0].body, sdp);
1140 }
1141
1142 #[test]
1143 fn multipart_part_with_multiple_headers() {
1144 let eido = b"<EmergencyCallData/>";
1145
1146 let mut body = Vec::new();
1147 body.extend_from_slice(b"--hdr-boundary\r\n");
1148 body.extend_from_slice(b"Content-Type: application/emergencyCallData.eido+xml\r\n");
1149 body.extend_from_slice(b"Content-ID: <eido@example.com>\r\n");
1150 body.extend_from_slice(b"Content-Disposition: by-reference\r\n");
1151 body.extend_from_slice(b"\r\n");
1152 body.extend_from_slice(eido);
1153 body.extend_from_slice(b"\r\n--hdr-boundary--");
1154
1155 let mut content = Vec::new();
1156 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1157 content.extend_from_slice(b"Call-ID: multi-hdr-part@host\r\n");
1158 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=hdr-boundary\r\n");
1159 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1160 content.extend_from_slice(b"\r\n");
1161 content.extend_from_slice(&body);
1162
1163 let msg = make_sip_message(&content);
1164 let parsed = msg.parse().unwrap();
1165 let parts = parsed.body_parts().unwrap();
1166 assert_eq!(parts.len(), 1);
1167 assert_eq!(
1168 parts[0].content_type(),
1169 Some("application/emergencyCallData.eido+xml")
1170 );
1171 assert_eq!(parts[0].content_id(), Some("<eido@example.com>"));
1172 assert_eq!(parts[0].content_disposition(), Some("by-reference"));
1173 assert_eq!(parts[0].body, eido);
1174 }
1175
1176 #[test]
1177 fn not_multipart_returns_none() {
1178 let content = b"INVITE sip:host SIP/2.0\r\n\
1179 Call-ID: not-multi@host\r\n\
1180 Content-Type: application/sdp\r\n\
1181 Content-Length: 4\r\n\
1182 \r\n\
1183 v=0\n";
1184 let msg = make_sip_message(content);
1185 let parsed = msg.parse().unwrap();
1186
1187 assert!(!parsed.is_multipart());
1188 assert!(parsed.multipart_boundary().is_none());
1189 assert!(parsed.body_parts().is_none());
1190 }
1191
1192 #[test]
1193 fn multipart_empty_body() {
1194 let mut content = Vec::new();
1195 content.extend_from_slice(b"INVITE sip:host SIP/2.0\r\n");
1196 content.extend_from_slice(b"Call-ID: empty-multi@host\r\n");
1197 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=empty\r\n");
1198 content.extend_from_slice(b"Content-Length: 9\r\n");
1199 content.extend_from_slice(b"\r\n");
1200 content.extend_from_slice(b"--empty--");
1201
1202 let msg = make_sip_message(&content);
1203 let parsed = msg.parse().unwrap();
1204 let parts = parsed.body_parts().unwrap();
1205 assert!(parts.is_empty());
1206 }
1207
1208 #[test]
1209 fn extract_boundary_unquoted() {
1210 assert_eq!(
1211 extract_boundary("multipart/mixed;boundary=foo-bar"),
1212 Some("foo-bar")
1213 );
1214 }
1215
1216 #[test]
1217 fn extract_boundary_quoted() {
1218 assert_eq!(
1219 extract_boundary("multipart/mixed; boundary=\"foo-bar\""),
1220 Some("foo-bar")
1221 );
1222 }
1223
1224 #[test]
1225 fn extract_boundary_with_extra_params() {
1226 assert_eq!(
1227 extract_boundary("multipart/mixed; boundary=foo;charset=utf-8"),
1228 Some("foo")
1229 );
1230 }
1231
1232 #[test]
1233 fn extract_boundary_case_insensitive() {
1234 assert_eq!(
1235 extract_boundary("multipart/mixed;BOUNDARY=abc"),
1236 Some("abc")
1237 );
1238 }
1239
1240 #[test]
1241 fn extract_boundary_missing() {
1242 assert_eq!(extract_boundary("multipart/mixed"), None);
1243 }
1244
1245 #[test]
1246 fn multipart_part_no_headers() {
1247 let raw_body = b"just raw content";
1248
1249 let mut body = Vec::new();
1250 body.extend_from_slice(b"--no-hdr\r\n");
1251 body.extend_from_slice(raw_body);
1252 body.extend_from_slice(b"\r\n--no-hdr--");
1253
1254 let mut content = Vec::new();
1255 content.extend_from_slice(b"MESSAGE sip:host SIP/2.0\r\n");
1256 content.extend_from_slice(b"Call-ID: no-hdr-part@host\r\n");
1257 content.extend_from_slice(b"Content-Type: multipart/mixed;boundary=no-hdr\r\n");
1258 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1259 content.extend_from_slice(b"\r\n");
1260 content.extend_from_slice(&body);
1261
1262 let msg = make_sip_message(&content);
1263 let parsed = msg.parse().unwrap();
1264 let parts = parsed.body_parts().unwrap();
1265 assert_eq!(parts.len(), 1);
1266 assert!(parts[0].content_type().is_none());
1267 assert!(parts[0].headers.is_empty());
1268 assert_eq!(parts[0].body, raw_body);
1269 }
1270
1271 #[test]
1274 fn is_json_content_type_application_json() {
1275 assert!(is_json_content_type("application/json"));
1276 }
1277
1278 #[test]
1279 fn is_json_content_type_plus_json() {
1280 assert!(is_json_content_type(
1281 "application/emergencyCallData.AbandonedCall+json"
1282 ));
1283 }
1284
1285 #[test]
1286 fn is_json_content_type_with_params() {
1287 assert!(is_json_content_type("application/json; charset=utf-8"));
1288 }
1289
1290 #[test]
1291 fn is_json_content_type_case_insensitive() {
1292 assert!(is_json_content_type("Application/JSON"));
1293 }
1294
1295 #[test]
1296 fn is_json_content_type_not_text_plain() {
1297 assert!(!is_json_content_type("text/plain"));
1298 }
1299
1300 #[test]
1301 fn is_json_content_type_not_multipart() {
1302 assert!(!is_json_content_type("multipart/mixed;boundary=foo"));
1303 }
1304
1305 #[test]
1306 fn is_json_content_type_not_sdp() {
1307 assert!(!is_json_content_type("application/sdp"));
1308 }
1309
1310 #[test]
1313 fn unescape_json_basic_escapes() {
1314 let input = br#"{"key":"line1\r\nline2\ttab\"\\"}"#;
1315 let result = unescape_json_body(input);
1316 assert!(
1317 result.contains("line1\r\nline2\ttab\"\\"),
1318 "basic escapes not unescaped: {result:?}"
1319 );
1320 }
1321
1322 #[test]
1323 fn unescape_json_slash_and_control() {
1324 let input = br#"{"a":"\/path","b":"\b\f"}"#;
1325 let result = unescape_json_body(input);
1326 assert!(result.contains("/path"), "\\/ should become /");
1327 assert!(result.contains('\x08'), "\\b should become backspace");
1328 assert!(result.contains('\x0C'), "\\f should become form feed");
1329 }
1330
1331 #[test]
1332 fn unescape_json_unicode_basic() {
1333 let input = br#"{"x":"\u0041"}"#;
1335 let result = unescape_json_body(input);
1336 assert!(
1337 result.contains('A'),
1338 "\\u0041 should become 'A': {result:?}"
1339 );
1340 }
1341
1342 #[test]
1343 fn unescape_json_unicode_surrogate_pair() {
1344 let input = br#"{"emoji":"\uD83D\uDE00"}"#;
1346 let result = unescape_json_body(input);
1347 assert!(
1348 result.contains('\u{1F600}'),
1349 "surrogate pair should produce U+1F600: {result:?}"
1350 );
1351 }
1352
1353 #[test]
1354 fn unescape_json_passthrough_non_escape() {
1355 let input = b"no escapes here";
1356 let result = unescape_json_body(input);
1357 assert_eq!(result, "no escapes here");
1358 }
1359
1360 #[test]
1363 fn json_field_extract_string() {
1364 let body = br#"{"event":"AbandonedCall","id":"123"}"#;
1365 let mut content = Vec::new();
1366 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
1367 content.extend_from_slice(b"Call-ID: jf-test@host\r\n");
1368 content.extend_from_slice(b"Content-Type: application/json\r\n");
1369 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1370 content.extend_from_slice(b"\r\n");
1371 content.extend_from_slice(body);
1372
1373 let msg = make_sip_message(&content);
1374 let parsed = msg.parse().unwrap();
1375
1376 assert_eq!(
1377 parsed.json_field("event"),
1378 Some("AbandonedCall".to_string())
1379 );
1380 assert_eq!(parsed.json_field("id"), Some("123".to_string()));
1381 }
1382
1383 #[test]
1384 fn json_field_missing_key() {
1385 let body = br#"{"event":"AbandonedCall"}"#;
1386 let mut content = Vec::new();
1387 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
1388 content.extend_from_slice(b"Call-ID: jf-miss@host\r\n");
1389 content.extend_from_slice(b"Content-Type: application/json\r\n");
1390 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1391 content.extend_from_slice(b"\r\n");
1392 content.extend_from_slice(body);
1393
1394 let msg = make_sip_message(&content);
1395 let parsed = msg.parse().unwrap();
1396
1397 assert_eq!(parsed.json_field("nonexistent"), None);
1398 }
1399
1400 #[test]
1401 fn json_field_non_string_value() {
1402 let body = br#"{"count":42,"active":true}"#;
1403 let mut content = Vec::new();
1404 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
1405 content.extend_from_slice(b"Call-ID: jf-nonstr@host\r\n");
1406 content.extend_from_slice(b"Content-Type: application/json\r\n");
1407 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1408 content.extend_from_slice(b"\r\n");
1409 content.extend_from_slice(body);
1410
1411 let msg = make_sip_message(&content);
1412 let parsed = msg.parse().unwrap();
1413
1414 assert_eq!(parsed.json_field("count"), None);
1415 assert_eq!(parsed.json_field("active"), None);
1416 }
1417
1418 #[test]
1419 fn json_field_non_json_content_type() {
1420 let body = br#"{"event":"AbandonedCall"}"#;
1421 let mut content = Vec::new();
1422 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
1423 content.extend_from_slice(b"Call-ID: jf-nonjson@host\r\n");
1424 content.extend_from_slice(b"Content-Type: text/plain\r\n");
1425 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1426 content.extend_from_slice(b"\r\n");
1427 content.extend_from_slice(body);
1428
1429 let msg = make_sip_message(&content);
1430 let parsed = msg.parse().unwrap();
1431
1432 assert_eq!(parsed.json_field("event"), None);
1433 }
1434
1435 #[test]
1436 fn json_field_unescapes_value() {
1437 let body = br#"{"invite":"INVITE sip:host\r\nTo: <sip:host>\r\n"}"#;
1438 let mut content = Vec::new();
1439 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
1440 content.extend_from_slice(b"Call-ID: jf-unescape@host\r\n");
1441 content.extend_from_slice(b"Content-Type: application/json\r\n");
1442 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1443 content.extend_from_slice(b"\r\n");
1444 content.extend_from_slice(body);
1445
1446 let msg = make_sip_message(&content);
1447 let parsed = msg.parse().unwrap();
1448
1449 let invite = parsed.json_field("invite").unwrap();
1450 assert!(
1451 invite.contains("INVITE sip:host\r\nTo: <sip:host>\r\n"),
1452 "json_field should return unescaped string: {invite:?}"
1453 );
1454 }
1455
1456 #[test]
1457 fn json_field_plus_json_content_type() {
1458 let body = br#"{"cancelTimestamp":"2025-12-14T05:35:03.269Z"}"#;
1459 let mut content = Vec::new();
1460 content.extend_from_slice(b"NOTIFY sip:host SIP/2.0\r\n");
1461 content.extend_from_slice(b"Call-ID: jf-plus@host\r\n");
1462 content.extend_from_slice(
1463 b"Content-Type: application/emergencyCallData.AbandonedCall+json\r\n",
1464 );
1465 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
1466 content.extend_from_slice(b"\r\n");
1467 content.extend_from_slice(body);
1468
1469 let msg = make_sip_message(&content);
1470 let parsed = msg.parse().unwrap();
1471
1472 assert_eq!(
1473 parsed.json_field("cancelTimestamp"),
1474 Some("2025-12-14T05:35:03.269Z".to_string())
1475 );
1476 }
1477
1478 #[test]
1479 fn whitespace_only_returns_transport_noise() {
1480 use crate::frame::ParseError;
1481
1482 for content in [b"\n".as_slice(), b"\r\n", b"\n\n\n", b" \t\r\n"] {
1483 let msg = SipMessage {
1484 direction: Direction::Recv,
1485 transport: Transport::Tls,
1486 address: "[10.0.0.1]:5061".into(),
1487 timestamp: Timestamp::TimeOnly {
1488 hour: 0,
1489 min: 0,
1490 sec: 0,
1491 usec: 0,
1492 },
1493 content: content.to_vec(),
1494 frame_count: 1,
1495 };
1496 let err = msg.parse().unwrap_err();
1497 assert!(
1498 matches!(err, ParseError::TransportNoise { .. }),
1499 "whitespace-only content {:?} should produce TransportNoise, got: {err}",
1500 content,
1501 );
1502 }
1503 }
1504}