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