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