1use std::borrow::Cow;
2use std::fmt;
3use std::net::SocketAddr;
4
5pub(crate) fn expand_compact(name: &str) -> Option<&'static str> {
8 let [ch] = name.as_bytes() else {
9 return None;
10 };
11 sip_header::SipHeader::from_compact(*ch).map(|header| header.as_str())
12}
13
14pub(crate) fn value_or_compact<'a>(headers: &'a Headers, name: &str) -> Option<&'a str> {
17 let mut compact = None;
18 for (key, value) in headers.iter() {
19 if key.eq_ignore_ascii_case(name) {
20 return Some(value);
21 }
22 if compact.is_none()
23 && key.len() == 1
24 && expand_compact(key).is_some_and(|full| full.eq_ignore_ascii_case(name))
25 {
26 compact = Some(value.as_str());
27 }
28 }
29 compact
30}
31
32fn parse_socket_addr(address: &str) -> Option<SocketAddr> {
35 if let Ok(addr) = address.parse() {
36 return Some(addr);
37 }
38 let (ip, port) = address.strip_prefix('[')?.split_once("]:")?;
39 Some(SocketAddr::new(ip.parse().ok()?, port.parse().ok()?))
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum SkipReason {
48 PartialFirstFrame,
52 OversizedFrame,
56 MidStreamSkip,
58 ReplayedFrame,
61 IncompleteFrame,
63 InvalidHeader,
65}
66
67impl fmt::Display for SkipReason {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 SkipReason::PartialFirstFrame => f.write_str("partial first frame"),
71 SkipReason::OversizedFrame => f.write_str("oversized frame"),
72 SkipReason::MidStreamSkip => f.write_str("mid-stream skip"),
73 SkipReason::ReplayedFrame => f.write_str("replayed frame (logrotate)"),
74 SkipReason::IncompleteFrame => f.write_str("incomplete frame"),
75 SkipReason::InvalidHeader => f.write_str("invalid header"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SkipTracking {
86 CountOnly,
88 TrackRegions,
90 CaptureData,
92}
93
94#[derive(Debug, Clone)]
96pub struct UnparsedRegion {
97 pub offset: u64,
99 pub length: u64,
101 pub reason: SkipReason,
103 pub data: Option<Vec<u8>>,
105}
106
107#[derive(Debug, Default, Clone)]
113pub struct ParseStats {
114 pub bytes_read: u64,
116 pub bytes_skipped: u64,
118 pub unparsed_regions: Vec<UnparsedRegion>,
121}
122
123impl ParseStats {
124 pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
126 std::mem::take(&mut self.unparsed_regions)
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct UnknownKeyword(String);
134
135impl UnknownKeyword {
136 pub fn as_str(&self) -> &str {
138 &self.0
139 }
140}
141
142impl fmt::Display for UnknownKeyword {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(f, "unknown keyword: {}", self.0)
145 }
146}
147
148impl std::error::Error for UnknownKeyword {}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum Direction {
153 Recv,
155 Sent,
157}
158
159impl fmt::Display for Direction {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.write_str(self.as_str())
162 }
163}
164
165impl std::str::FromStr for Direction {
166 type Err = UnknownKeyword;
167
168 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 for candidate in [Direction::Recv, Direction::Sent] {
171 if s.eq_ignore_ascii_case(candidate.as_str()) {
172 return Ok(candidate);
173 }
174 }
175 Err(UnknownKeyword(s.to_string()))
176 }
177}
178
179impl Direction {
180 pub fn as_str(&self) -> &'static str {
182 match self {
183 Direction::Recv => "recv",
184 Direction::Sent => "sent",
185 }
186 }
187
188 pub fn preposition(&self) -> &'static str {
190 match self {
191 Direction::Recv => "from",
192 Direction::Sent => "to",
193 }
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
199pub enum Transport {
200 Tcp,
202 Udp,
204 Tls,
206 Wss,
208}
209
210impl Transport {
211 pub fn as_str(&self) -> &'static str {
213 match self {
214 Transport::Tcp => "tcp",
215 Transport::Udp => "udp",
216 Transport::Tls => "tls",
217 Transport::Wss => "wss",
218 }
219 }
220}
221
222impl fmt::Display for Transport {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 f.write_str(self.as_str())
225 }
226}
227
228impl std::str::FromStr for Transport {
229 type Err = UnknownKeyword;
230
231 fn from_str(s: &str) -> Result<Self, Self::Err> {
233 for candidate in [
234 Transport::Tcp,
235 Transport::Udp,
236 Transport::Tls,
237 Transport::Wss,
238 ] {
239 if s.eq_ignore_ascii_case(candidate.as_str()) {
240 return Ok(candidate);
241 }
242 }
243 Err(UnknownKeyword(s.to_string()))
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum Timestamp {
253 TimeOnly {
255 hour: u8,
257 min: u8,
259 sec: u8,
261 usec: u32,
263 },
264 DateTime {
266 year: u16,
268 month: u8,
270 day: u8,
272 hour: u8,
274 min: u8,
276 sec: u8,
278 usec: u32,
280 },
281}
282
283impl Timestamp {
284 pub fn time_of_day_secs(&self) -> u32 {
286 let (h, m, s) = match self {
287 Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
288 Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
289 };
290 h as u32 * 3600 + m as u32 * 60 + s as u32
291 }
292
293 pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
296 match self {
297 Timestamp::TimeOnly {
298 hour,
299 min,
300 sec,
301 usec,
302 } => (0, 0, 0, *hour, *min, *sec, *usec),
303 Timestamp::DateTime {
304 year,
305 month,
306 day,
307 hour,
308 min,
309 sec,
310 usec,
311 } => (*year, *month, *day, *hour, *min, *sec, *usec),
312 }
313 }
314}
315
316pub(crate) fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
319 let y = if m <= 2 { y - 1 } else { y };
320 let era = if y >= 0 { y } else { y - 399 } / 400;
321 let yoe = (y - era * 400) as u64;
322 let m_adj = if m > 2 { m as i64 - 3 } else { m as i64 + 9 } as u64;
323 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
324 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
325 era * 146097 + doe as i64 - 719468
326}
327
328#[derive(Debug, Default, Clone)]
338pub struct StaleClock {
339 day: u32,
340 last_time_secs: u32,
341 now: u64,
342 last_sweep: u64,
343 dated: Option<bool>,
344 reset: bool,
345}
346
347impl StaleClock {
348 pub const TIMEOUT_SECS: u64 = 7200;
352
353 pub fn new() -> Self {
355 Self::default()
356 }
357
358 pub fn observe(&mut self, timestamp: Timestamp) -> u64 {
360 let dated = matches!(timestamp, Timestamp::DateTime { .. });
361 if self.dated != Some(dated) {
362 self.dated = Some(dated);
363 self.day = 0;
364 self.last_time_secs = 0;
365 self.reset = true;
366 }
367
368 let time_secs = timestamp.time_of_day_secs();
369 self.now = match timestamp {
370 Timestamp::DateTime {
371 year, month, day, ..
372 } => {
373 let days = days_from_civil(year as i64, month as u32, day as u32).max(0) as u64;
374 days * 86400 + time_secs as u64
375 }
376 Timestamp::TimeOnly { .. } => {
377 if time_secs < self.last_time_secs && self.last_time_secs - time_secs > 43200 {
378 self.day += 1;
379 }
380 self.day as u64 * 86400 + time_secs as u64
381 }
382 };
383 self.last_time_secs = time_secs;
384 self.now
385 }
386
387 pub fn now(&self) -> u64 {
389 self.now
390 }
391
392 pub fn sweep_due(&mut self) -> bool {
396 if self.reset {
397 self.reset = false;
398 self.last_sweep = self.now;
399 return false;
400 }
401 if self.now.saturating_sub(self.last_sweep) >= Self::TIMEOUT_SECS {
402 self.last_sweep = self.now;
403 return true;
404 }
405 false
406 }
407
408 pub fn is_stale(&self, last_seen: u64) -> bool {
410 self.now.saturating_sub(last_seen) > Self::TIMEOUT_SECS
411 }
412}
413
414impl fmt::Display for Timestamp {
415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 match self {
417 Timestamp::TimeOnly {
418 hour,
419 min,
420 sec,
421 usec,
422 } => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
423 Timestamp::DateTime {
424 year,
425 month,
426 day,
427 hour,
428 min,
429 sec,
430 usec,
431 } => write!(
432 f,
433 "{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
434 ),
435 }
436 }
437}
438
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub struct FrameMeta<'a> {
446 pub direction: Direction,
448 pub transport: Transport,
450 pub address: &'a str,
452 pub timestamp: Timestamp,
454}
455
456impl FrameMeta<'_> {
457 pub fn socket_addr(&self) -> Option<SocketAddr> {
461 parse_socket_addr(self.address)
462 }
463}
464
465impl fmt::Display for FrameMeta<'_> {
466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467 write!(
468 f,
469 "{} {} {}/{} at {}",
470 self.direction,
471 self.direction.preposition(),
472 self.transport,
473 self.address,
474 self.timestamp
475 )
476 }
477}
478
479#[derive(Debug, Clone)]
485pub struct Frame {
486 pub direction: Direction,
488 pub byte_count: usize,
490 pub transport: Transport,
492 pub address: String,
494 pub timestamp: Timestamp,
496 pub content: Vec<u8>,
498}
499
500impl Frame {
501 pub fn meta(&self) -> FrameMeta<'_> {
503 FrameMeta {
504 direction: self.direction,
505 transport: self.transport,
506 address: &self.address,
507 timestamp: self.timestamp,
508 }
509 }
510
511 pub fn socket_addr(&self) -> Option<SocketAddr> {
515 self.meta().socket_addr()
516 }
517}
518
519#[derive(Debug, Clone)]
524pub struct SipMessage {
525 pub direction: Direction,
527 pub transport: Transport,
529 pub address: String,
531 pub timestamp: Timestamp,
533 pub content: Vec<u8>,
535 pub frame_count: usize,
537}
538
539impl SipMessage {
540 pub fn meta(&self) -> FrameMeta<'_> {
542 FrameMeta {
543 direction: self.direction,
544 transport: self.transport,
545 address: &self.address,
546 timestamp: self.timestamp,
547 }
548 }
549
550 pub fn socket_addr(&self) -> Option<SocketAddr> {
554 self.meta().socket_addr()
555 }
556}
557
558#[derive(Debug, Clone, PartialEq, Eq)]
560pub enum SipMessageType {
561 Request {
563 method: String,
565 uri: String,
567 },
568 Response {
570 code: u16,
572 reason: String,
574 },
575}
576
577impl fmt::Display for SipMessageType {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 match self {
580 SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
581 SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
582 }
583 }
584}
585
586impl SipMessageType {
587 pub fn summary(&self) -> Cow<'_, str> {
589 match self {
590 SipMessageType::Request { method, .. } => Cow::Borrowed(method),
591 SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
592 }
593 }
594}
595
596#[derive(Debug, Clone, Default, PartialEq, Eq)]
599pub struct Headers(pub Vec<(String, String)>);
600
601impl Headers {
602 pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
605 self.0
606 .iter()
607 .filter(move |(k, _)| k.eq_ignore_ascii_case(name))
608 .map(|(_, v)| v.as_str())
609 }
610
611 pub fn value(&self, name: &str) -> Option<&str> {
615 self.0
616 .iter()
617 .find(|(k, _)| k.eq_ignore_ascii_case(name))
618 .map(|(_, v)| v.as_str())
619 }
620}
621
622impl std::ops::Deref for Headers {
623 type Target = [(String, String)];
624
625 fn deref(&self) -> &Self::Target {
626 &self.0
627 }
628}
629
630impl std::ops::DerefMut for Headers {
631 fn deref_mut(&mut self) -> &mut Self::Target {
632 &mut self.0
633 }
634}
635
636impl From<Vec<(String, String)>> for Headers {
637 fn from(headers: Vec<(String, String)>) -> Self {
638 Headers(headers)
639 }
640}
641
642impl FromIterator<(String, String)> for Headers {
643 fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
644 Headers(iter.into_iter().collect())
645 }
646}
647
648impl<'a> IntoIterator for &'a Headers {
649 type Item = &'a (String, String);
650 type IntoIter = std::slice::Iter<'a, (String, String)>;
651
652 fn into_iter(self) -> Self::IntoIter {
653 self.0.iter()
654 }
655}
656
657#[derive(Debug, Clone)]
664pub struct ParsedSipMessage {
665 pub direction: Direction,
667 pub transport: Transport,
669 pub address: String,
671 pub timestamp: Timestamp,
673 pub message_type: SipMessageType,
675 pub headers: Headers,
677 pub body: Vec<u8>,
679 pub frame_count: usize,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq)]
690pub struct SipFragment {
691 pub message_type: Option<SipMessageType>,
693 pub headers: Headers,
695 pub body: Vec<u8>,
697}
698
699impl SipFragment {
700 pub fn header_value(&self, name: &str) -> Option<&str> {
702 self.headers.value(name)
703 }
704
705 pub fn content_type(&self) -> Option<&str> {
708 value_or_compact(&self.headers, "Content-Type")
709 }
710}
711
712#[derive(Debug, Clone, PartialEq, Eq)]
714pub struct MimePart {
715 pub headers: Headers,
717 pub body: Vec<u8>,
719}
720
721impl MimePart {
722 pub fn content_type(&self) -> Option<&str> {
725 value_or_compact(&self.headers, "Content-Type")
726 }
727
728 pub fn header_value(&self, name: &str) -> Option<&str> {
730 self.headers.value(name)
731 }
732
733 pub fn content_id(&self) -> Option<&str> {
735 self.header_value("Content-ID")
736 }
737
738 pub fn content_disposition(&self) -> Option<&str> {
740 self.header_value("Content-Disposition")
741 }
742
743 pub fn content_transfer_encoding(&self) -> Option<&str> {
747 self.header_value("Content-Transfer-Encoding")
748 }
749}
750
751impl ParsedSipMessage {
752 pub fn meta(&self) -> FrameMeta<'_> {
754 FrameMeta {
755 direction: self.direction,
756 transport: self.transport,
757 address: &self.address,
758 timestamp: self.timestamp,
759 }
760 }
761
762 pub fn socket_addr(&self) -> Option<SocketAddr> {
766 self.meta().socket_addr()
767 }
768
769 pub fn call_id(&self) -> Option<&str> {
772 value_or_compact(&self.headers, "Call-ID")
773 }
774
775 pub fn content_type(&self) -> Option<&str> {
778 value_or_compact(&self.headers, "Content-Type")
779 }
780
781 pub fn content_length(&self) -> Option<usize> {
784 value_or_compact(&self.headers, "Content-Length").and_then(|v| v.trim().parse().ok())
785 }
786
787 pub fn cseq(&self) -> Option<&str> {
789 self.header_value("CSeq")
790 }
791
792 pub fn method(&self) -> Option<&str> {
795 match &self.message_type {
796 SipMessageType::Request { method, .. } => Some(method),
797 SipMessageType::Response { .. } => {
798 self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
799 }
800 }
801 }
802
803 pub fn body_data(&self) -> Cow<'_, str> {
806 String::from_utf8_lossy(&self.body)
807 }
808
809 pub fn to_bytes(&self) -> Vec<u8> {
811 let mut out = Vec::new();
812 match &self.message_type {
813 SipMessageType::Request { method, uri } => {
814 out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
815 }
816 SipMessageType::Response { code, reason } => {
817 out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
818 }
819 }
820 for (name, value) in &self.headers {
821 out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
822 }
823 out.extend_from_slice(b"\r\n");
824 out.extend_from_slice(&self.body);
825 out
826 }
827
828 pub fn header_value(&self, name: &str) -> Option<&str> {
831 self.headers.value(name)
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838
839 fn make_parsed(
840 msg_type: SipMessageType,
841 headers: Vec<(&str, &str)>,
842 body: &[u8],
843 ) -> ParsedSipMessage {
844 ParsedSipMessage {
845 direction: Direction::Recv,
846 transport: Transport::Tcp,
847 address: "10.0.0.1:5060".into(),
848 timestamp: Timestamp::TimeOnly {
849 hour: 12,
850 min: 0,
851 sec: 0,
852 usec: 0,
853 },
854 message_type: msg_type,
855 headers: Headers(
856 headers
857 .iter()
858 .map(|(k, v)| (k.to_string(), v.to_string()))
859 .collect(),
860 ),
861 body: body.to_vec(),
862 frame_count: 1,
863 }
864 }
865
866 fn make_frame(address: &str) -> Frame {
867 Frame {
868 direction: Direction::Recv,
869 byte_count: 0,
870 transport: Transport::Tcp,
871 address: address.into(),
872 timestamp: Timestamp::TimeOnly {
873 hour: 0,
874 min: 0,
875 sec: 0,
876 usec: 0,
877 },
878 content: Vec::new(),
879 }
880 }
881
882 fn make_message(address: &str) -> SipMessage {
883 SipMessage {
884 direction: Direction::Recv,
885 transport: Transport::Tcp,
886 address: address.into(),
887 timestamp: Timestamp::TimeOnly {
888 hour: 0,
889 min: 0,
890 sec: 0,
891 usec: 0,
892 },
893 content: Vec::new(),
894 frame_count: 1,
895 }
896 }
897
898 fn parsed_with_address(address: &str) -> ParsedSipMessage {
899 let mut msg = make_parsed(
900 SipMessageType::Request {
901 method: "OPTIONS".into(),
902 uri: "sip:host".into(),
903 },
904 vec![],
905 b"",
906 );
907 msg.address = address.into();
908 msg
909 }
910
911 #[test]
912 fn socket_addr_ipv4() {
913 let addr = make_frame("10.0.0.1:5060").socket_addr().unwrap();
914 assert!(addr.is_ipv4());
915 assert_eq!(addr.port(), 5060);
916 assert_eq!(addr.ip().to_string(), "10.0.0.1");
917 }
918
919 #[test]
920 fn socket_addr_ipv6_bracketed() {
921 let addr = make_message("[2001:db8::1]:5061").socket_addr().unwrap();
922 assert!(addr.is_ipv6());
923 assert_eq!(addr.port(), 5061);
924 assert_eq!(addr.ip().to_string(), "2001:db8::1");
925 }
926
927 #[test]
928 fn socket_addr_ipv4_bracketed() {
929 let addr = make_frame("[198.51.100.7]:5060").socket_addr().unwrap();
930 assert!(addr.is_ipv4());
931 assert_eq!(addr.port(), 5060);
932 assert_eq!(addr.ip().to_string(), "198.51.100.7");
933 }
934
935 #[test]
936 fn socket_addr_on_parsed_message() {
937 let addr = parsed_with_address("192.0.2.4:5080").socket_addr().unwrap();
938 assert_eq!(addr.port(), 5080);
939 }
940
941 #[test]
942 fn socket_addr_rejects_non_addresses() {
943 for bad in [
944 "345.678.987.654:5060",
945 "10.0.0.1",
946 "host.example.test:5060",
947 "2001:db8::1:5060",
948 "",
949 ] {
950 assert!(
951 make_frame(bad).socket_addr().is_none(),
952 "should not parse: {bad}"
953 );
954 assert!(make_message(bad).socket_addr().is_none());
955 assert!(parsed_with_address(bad).socket_addr().is_none());
956 }
957 }
958
959 #[test]
960 fn to_bytes_request_no_body() {
961 let msg = make_parsed(
962 SipMessageType::Request {
963 method: "OPTIONS".into(),
964 uri: "sip:host".into(),
965 },
966 vec![("Call-ID", "test")],
967 b"",
968 );
969 let bytes = msg.to_bytes();
970 let text = String::from_utf8(bytes).unwrap();
971 assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
972 assert!(text.contains("Call-ID: test\r\n"));
973 assert!(text.ends_with("\r\n\r\n"));
974 }
975
976 #[test]
977 fn to_bytes_request_with_body() {
978 let body = b"v=0\r\ns=-\r\n";
979 let msg = make_parsed(
980 SipMessageType::Request {
981 method: "INVITE".into(),
982 uri: "sip:host".into(),
983 },
984 vec![("Call-ID", "test")],
985 body,
986 );
987 let bytes = msg.to_bytes();
988 assert!(bytes.ends_with(body));
989 }
990
991 #[test]
992 fn to_bytes_response() {
993 let msg = make_parsed(
994 SipMessageType::Response {
995 code: 200,
996 reason: "OK".into(),
997 },
998 vec![("Call-ID", "resp-test")],
999 b"",
1000 );
1001 let bytes = msg.to_bytes();
1002 let text = String::from_utf8(bytes).unwrap();
1003 assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
1004 }
1005
1006 #[test]
1007 fn body_data_valid_utf8() {
1008 let msg = make_parsed(
1009 SipMessageType::Request {
1010 method: "MESSAGE".into(),
1011 uri: "sip:host".into(),
1012 },
1013 vec![],
1014 b"hello world",
1015 );
1016 assert_eq!(&*msg.body_data(), "hello world");
1017 }
1018
1019 #[test]
1020 fn body_data_empty() {
1021 let msg = make_parsed(
1022 SipMessageType::Request {
1023 method: "OPTIONS".into(),
1024 uri: "sip:host".into(),
1025 },
1026 vec![],
1027 b"",
1028 );
1029 assert_eq!(&*msg.body_data(), "");
1030 }
1031
1032 #[test]
1033 fn body_data_binary() {
1034 let msg = make_parsed(
1035 SipMessageType::Request {
1036 method: "MESSAGE".into(),
1037 uri: "sip:host".into(),
1038 },
1039 vec![],
1040 &[0xFF, 0xFE],
1041 );
1042 assert!(msg.body_data().contains('\u{FFFD}'));
1043 }
1044
1045 #[test]
1046 fn body_data_preserves_json_escapes() {
1047 let raw = br#"{"key":"value\nwith\\escapes"}"#;
1048 let msg = make_parsed(
1049 SipMessageType::Request {
1050 method: "NOTIFY".into(),
1051 uri: "sip:host".into(),
1052 },
1053 vec![("Content-Type", "application/json")],
1054 raw,
1055 );
1056 assert_eq!(
1057 msg.body_data().as_ref(),
1058 r#"{"key":"value\nwith\\escapes"}"#,
1059 "body_data() must preserve raw escapes"
1060 );
1061 }
1062}