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)]
47#[non_exhaustive]
48pub enum SkipReason {
49 PartialFirstFrame,
53 OversizedFrame,
57 MidStreamSkip,
59 ReplayedFrame,
62 IncompleteFrame,
64 InvalidHeader,
66}
67
68impl fmt::Display for SkipReason {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 SkipReason::PartialFirstFrame => f.write_str("partial first frame"),
72 SkipReason::OversizedFrame => f.write_str("oversized frame"),
73 SkipReason::MidStreamSkip => f.write_str("mid-stream skip"),
74 SkipReason::ReplayedFrame => f.write_str("replayed frame (logrotate)"),
75 SkipReason::IncompleteFrame => f.write_str("incomplete frame"),
76 SkipReason::InvalidHeader => f.write_str("invalid header"),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SkipTracking {
87 CountOnly,
89 TrackRegions,
91 CaptureData,
93}
94
95#[derive(Debug, Clone)]
97#[non_exhaustive]
98pub struct UnparsedRegion {
99 pub offset: u64,
101 pub length: u64,
103 pub reason: SkipReason,
105 pub data: Option<Vec<u8>>,
107}
108
109#[derive(Debug, Default, Clone)]
115pub struct ParseStats {
116 pub(crate) bytes_read: u64,
117 pub(crate) bytes_skipped: u64,
118 pub(crate) incomplete_frames: u64,
119 pub(crate) incomplete_frame_bytes: u64,
120 pub(crate) unparsed_regions: Vec<UnparsedRegion>,
121}
122
123impl ParseStats {
124 pub fn bytes_read(&self) -> u64 {
126 self.bytes_read
127 }
128
129 pub fn bytes_skipped(&self) -> u64 {
131 self.bytes_skipped
132 }
133
134 pub fn incomplete_frames(&self) -> u64 {
137 self.incomplete_frames
138 }
139
140 pub fn incomplete_frame_bytes(&self) -> u64 {
143 self.incomplete_frame_bytes
144 }
145
146 pub fn unparsed_regions(&self) -> &[UnparsedRegion] {
149 &self.unparsed_regions
150 }
151
152 pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
154 std::mem::take(&mut self.unparsed_regions)
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct UnknownKeyword(String);
162
163impl UnknownKeyword {
164 pub fn as_str(&self) -> &str {
166 &self.0
167 }
168}
169
170impl fmt::Display for UnknownKeyword {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, "unknown keyword: {}", self.0)
173 }
174}
175
176impl std::error::Error for UnknownKeyword {}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180pub enum Direction {
181 Recv,
183 Sent,
185}
186
187impl fmt::Display for Direction {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.write_str(self.as_str())
190 }
191}
192
193impl std::str::FromStr for Direction {
194 type Err = UnknownKeyword;
195
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
198 for candidate in [Direction::Recv, Direction::Sent] {
199 if s.eq_ignore_ascii_case(candidate.as_str()) {
200 return Ok(candidate);
201 }
202 }
203 Err(UnknownKeyword(s.to_string()))
204 }
205}
206
207impl Direction {
208 pub fn as_str(&self) -> &'static str {
210 match self {
211 Direction::Recv => "recv",
212 Direction::Sent => "sent",
213 }
214 }
215
216 pub fn preposition(&self) -> &'static str {
218 match self {
219 Direction::Recv => "from",
220 Direction::Sent => "to",
221 }
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227pub enum Transport {
228 Tcp,
230 Udp,
232 Tls,
234 Wss,
236}
237
238impl Transport {
239 pub fn as_str(&self) -> &'static str {
241 match self {
242 Transport::Tcp => "tcp",
243 Transport::Udp => "udp",
244 Transport::Tls => "tls",
245 Transport::Wss => "wss",
246 }
247 }
248}
249
250impl fmt::Display for Transport {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.write_str(self.as_str())
253 }
254}
255
256impl std::str::FromStr for Transport {
257 type Err = UnknownKeyword;
258
259 fn from_str(s: &str) -> Result<Self, Self::Err> {
261 for candidate in [
262 Transport::Tcp,
263 Transport::Udp,
264 Transport::Tls,
265 Transport::Wss,
266 ] {
267 if s.eq_ignore_ascii_case(candidate.as_str()) {
268 return Ok(candidate);
269 }
270 }
271 Err(UnknownKeyword(s.to_string()))
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum Timestamp {
281 TimeOnly {
283 hour: u8,
285 min: u8,
287 sec: u8,
289 usec: u32,
291 },
292 DateTime {
294 year: u16,
296 month: u8,
298 day: u8,
300 hour: u8,
302 min: u8,
304 sec: u8,
306 usec: u32,
308 },
309}
310
311impl Timestamp {
312 pub fn time_of_day_secs(&self) -> u32 {
314 let (h, m, s) = match self {
315 Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
316 Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
317 };
318 h as u32 * 3600 + m as u32 * 60 + s as u32
319 }
320
321 pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
324 match self {
325 Timestamp::TimeOnly {
326 hour,
327 min,
328 sec,
329 usec,
330 } => (0, 0, 0, *hour, *min, *sec, *usec),
331 Timestamp::DateTime {
332 year,
333 month,
334 day,
335 hour,
336 min,
337 sec,
338 usec,
339 } => (*year, *month, *day, *hour, *min, *sec, *usec),
340 }
341 }
342}
343
344pub(crate) fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
347 let y = if m <= 2 { y - 1 } else { y };
348 let era = if y >= 0 { y } else { y - 399 } / 400;
349 let yoe = (y - era * 400) as u64;
350 let m_adj = if m > 2 { m as i64 - 3 } else { m as i64 + 9 } as u64;
351 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
352 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
353 era * 146097 + doe as i64 - 719468
354}
355
356#[derive(Debug, Default, Clone)]
366pub struct StaleClock {
367 day: u32,
368 last_time_secs: u32,
369 now: u64,
370 last_sweep: u64,
371 dated: Option<bool>,
372 reset: bool,
373}
374
375impl StaleClock {
376 pub const TIMEOUT_SECS: u64 = 7200;
380
381 pub fn new() -> Self {
383 Self::default()
384 }
385
386 pub fn observe(&mut self, timestamp: Timestamp) -> u64 {
388 let dated = matches!(timestamp, Timestamp::DateTime { .. });
389 if self.dated != Some(dated) {
390 self.dated = Some(dated);
391 self.day = 0;
392 self.last_time_secs = 0;
393 self.reset = true;
394 }
395
396 let time_secs = timestamp.time_of_day_secs();
397 self.now = match timestamp {
398 Timestamp::DateTime {
399 year, month, day, ..
400 } => {
401 let days = days_from_civil(year as i64, month as u32, day as u32).max(0) as u64;
402 days * 86400 + time_secs as u64
403 }
404 Timestamp::TimeOnly { .. } => {
405 if time_secs < self.last_time_secs && self.last_time_secs - time_secs > 43200 {
406 self.day += 1;
407 }
408 self.day as u64 * 86400 + time_secs as u64
409 }
410 };
411 self.last_time_secs = time_secs;
412 self.now
413 }
414
415 pub fn now(&self) -> u64 {
417 self.now
418 }
419
420 pub fn sweep_due(&mut self) -> bool {
424 if self.reset {
425 self.reset = false;
426 self.last_sweep = self.now;
427 return false;
428 }
429 if self.now.saturating_sub(self.last_sweep) >= Self::TIMEOUT_SECS {
430 self.last_sweep = self.now;
431 return true;
432 }
433 false
434 }
435
436 pub fn is_stale(&self, last_seen: u64) -> bool {
438 self.now.saturating_sub(last_seen) > Self::TIMEOUT_SECS
439 }
440}
441
442impl fmt::Display for Timestamp {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 match self {
445 Timestamp::TimeOnly {
446 hour,
447 min,
448 sec,
449 usec,
450 } => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
451 Timestamp::DateTime {
452 year,
453 month,
454 day,
455 hour,
456 min,
457 sec,
458 usec,
459 } => write!(
460 f,
461 "{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
462 ),
463 }
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473pub struct FrameMeta<'a> {
474 pub direction: Direction,
476 pub transport: Transport,
478 pub address: &'a str,
480 pub timestamp: Timestamp,
482}
483
484impl FrameMeta<'_> {
485 pub fn socket_addr(&self) -> Option<SocketAddr> {
489 parse_socket_addr(self.address)
490 }
491}
492
493impl fmt::Display for FrameMeta<'_> {
494 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495 write!(
496 f,
497 "{} {} {}/{} at {}",
498 self.direction,
499 self.direction.preposition(),
500 self.transport,
501 self.address,
502 self.timestamp
503 )
504 }
505}
506
507#[derive(Debug, Clone)]
513pub struct Frame {
514 pub direction: Direction,
516 pub byte_count: usize,
518 pub transport: Transport,
520 pub address: String,
522 pub timestamp: Timestamp,
524 pub content: Vec<u8>,
526}
527
528impl Frame {
529 pub fn meta(&self) -> FrameMeta<'_> {
531 FrameMeta {
532 direction: self.direction,
533 transport: self.transport,
534 address: &self.address,
535 timestamp: self.timestamp,
536 }
537 }
538
539 pub fn socket_addr(&self) -> Option<SocketAddr> {
543 self.meta().socket_addr()
544 }
545}
546
547#[derive(Debug, Clone)]
552pub struct SipMessage {
553 pub direction: Direction,
555 pub transport: Transport,
557 pub address: String,
559 pub timestamp: Timestamp,
561 pub content: Vec<u8>,
563 pub frame_count: usize,
565}
566
567impl SipMessage {
568 pub fn meta(&self) -> FrameMeta<'_> {
570 FrameMeta {
571 direction: self.direction,
572 transport: self.transport,
573 address: &self.address,
574 timestamp: self.timestamp,
575 }
576 }
577
578 pub fn socket_addr(&self) -> Option<SocketAddr> {
582 self.meta().socket_addr()
583 }
584}
585
586#[derive(Debug, Clone, PartialEq, Eq)]
588pub enum SipMessageType {
589 Request {
591 method: String,
593 uri: String,
595 },
596 Response {
598 code: u16,
600 reason: String,
602 },
603}
604
605impl fmt::Display for SipMessageType {
606 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
607 match self {
608 SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
609 SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
610 }
611 }
612}
613
614impl SipMessageType {
615 pub fn summary(&self) -> Cow<'_, str> {
617 match self {
618 SipMessageType::Request { method, .. } => Cow::Borrowed(method),
619 SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
620 }
621 }
622}
623
624#[derive(Debug, Clone, Default, PartialEq, Eq)]
627pub struct Headers(Vec<(String, String)>);
628
629impl Headers {
630 pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
633 self.0
634 .iter()
635 .filter(move |(k, _)| k.eq_ignore_ascii_case(name))
636 .map(|(_, v)| v.as_str())
637 }
638
639 pub fn value(&self, name: &str) -> Option<&str> {
643 self.0
644 .iter()
645 .find(|(k, _)| k.eq_ignore_ascii_case(name))
646 .map(|(_, v)| v.as_str())
647 }
648}
649
650impl std::ops::Deref for Headers {
651 type Target = [(String, String)];
652
653 fn deref(&self) -> &Self::Target {
654 &self.0
655 }
656}
657
658impl std::ops::DerefMut for Headers {
659 fn deref_mut(&mut self) -> &mut Self::Target {
660 &mut self.0
661 }
662}
663
664impl From<Vec<(String, String)>> for Headers {
665 fn from(headers: Vec<(String, String)>) -> Self {
666 Headers(headers)
667 }
668}
669
670impl FromIterator<(String, String)> for Headers {
671 fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
672 Headers(iter.into_iter().collect())
673 }
674}
675
676impl<'a> IntoIterator for &'a Headers {
677 type Item = &'a (String, String);
678 type IntoIter = std::slice::Iter<'a, (String, String)>;
679
680 fn into_iter(self) -> Self::IntoIter {
681 self.0.iter()
682 }
683}
684
685#[derive(Debug, Clone)]
692pub struct ParsedSipMessage {
693 pub direction: Direction,
695 pub transport: Transport,
697 pub address: String,
699 pub timestamp: Timestamp,
701 pub message_type: SipMessageType,
703 pub headers: Headers,
705 pub body: Vec<u8>,
707 pub frame_count: usize,
709}
710
711#[derive(Debug, Clone, PartialEq, Eq)]
718pub struct SipFragment {
719 pub message_type: Option<SipMessageType>,
721 pub headers: Headers,
723 pub body: Vec<u8>,
725}
726
727impl SipFragment {
728 pub fn header_value(&self, name: &str) -> Option<&str> {
730 self.headers.value(name)
731 }
732
733 pub fn content_type(&self) -> Option<&str> {
736 value_or_compact(&self.headers, "Content-Type")
737 }
738}
739
740#[derive(Debug, Clone, PartialEq, Eq)]
742pub struct MimePart {
743 pub headers: Headers,
745 pub body: Vec<u8>,
747}
748
749impl MimePart {
750 pub fn content_type(&self) -> Option<&str> {
753 value_or_compact(&self.headers, "Content-Type")
754 }
755
756 pub fn header_value(&self, name: &str) -> Option<&str> {
758 self.headers.value(name)
759 }
760
761 pub fn content_id(&self) -> Option<&str> {
763 self.header_value("Content-ID")
764 }
765
766 pub fn content_disposition(&self) -> Option<&str> {
768 self.header_value("Content-Disposition")
769 }
770
771 pub fn content_transfer_encoding(&self) -> Option<&str> {
775 self.header_value("Content-Transfer-Encoding")
776 }
777}
778
779impl ParsedSipMessage {
780 pub fn meta(&self) -> FrameMeta<'_> {
782 FrameMeta {
783 direction: self.direction,
784 transport: self.transport,
785 address: &self.address,
786 timestamp: self.timestamp,
787 }
788 }
789
790 pub fn socket_addr(&self) -> Option<SocketAddr> {
794 self.meta().socket_addr()
795 }
796
797 pub fn call_id(&self) -> Option<&str> {
800 value_or_compact(&self.headers, "Call-ID")
801 }
802
803 pub fn content_type(&self) -> Option<&str> {
806 value_or_compact(&self.headers, "Content-Type")
807 }
808
809 pub fn content_length(&self) -> Option<usize> {
812 value_or_compact(&self.headers, "Content-Length").and_then(|v| v.trim().parse().ok())
813 }
814
815 pub fn cseq(&self) -> Option<&str> {
817 self.header_value("CSeq")
818 }
819
820 pub fn method(&self) -> Option<&str> {
823 match &self.message_type {
824 SipMessageType::Request { method, .. } => Some(method),
825 SipMessageType::Response { .. } => {
826 self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
827 }
828 }
829 }
830
831 pub fn body_data(&self) -> Cow<'_, str> {
834 String::from_utf8_lossy(&self.body)
835 }
836
837 pub fn to_bytes(&self) -> Vec<u8> {
839 let mut out = Vec::new();
840 match &self.message_type {
841 SipMessageType::Request { method, uri } => {
842 out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
843 }
844 SipMessageType::Response { code, reason } => {
845 out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
846 }
847 }
848 for (name, value) in &self.headers {
849 out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
850 }
851 out.extend_from_slice(b"\r\n");
852 out.extend_from_slice(&self.body);
853 out
854 }
855
856 pub fn header_value(&self, name: &str) -> Option<&str> {
859 self.headers.value(name)
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866
867 fn make_parsed(
868 msg_type: SipMessageType,
869 headers: Vec<(&str, &str)>,
870 body: &[u8],
871 ) -> ParsedSipMessage {
872 ParsedSipMessage {
873 direction: Direction::Recv,
874 transport: Transport::Tcp,
875 address: "10.0.0.1:5060".into(),
876 timestamp: Timestamp::TimeOnly {
877 hour: 12,
878 min: 0,
879 sec: 0,
880 usec: 0,
881 },
882 message_type: msg_type,
883 headers: Headers(
884 headers
885 .iter()
886 .map(|(k, v)| (k.to_string(), v.to_string()))
887 .collect(),
888 ),
889 body: body.to_vec(),
890 frame_count: 1,
891 }
892 }
893
894 fn make_frame(address: &str) -> Frame {
895 Frame {
896 direction: Direction::Recv,
897 byte_count: 0,
898 transport: Transport::Tcp,
899 address: address.into(),
900 timestamp: Timestamp::TimeOnly {
901 hour: 0,
902 min: 0,
903 sec: 0,
904 usec: 0,
905 },
906 content: Vec::new(),
907 }
908 }
909
910 fn make_message(address: &str) -> SipMessage {
911 SipMessage {
912 direction: Direction::Recv,
913 transport: Transport::Tcp,
914 address: address.into(),
915 timestamp: Timestamp::TimeOnly {
916 hour: 0,
917 min: 0,
918 sec: 0,
919 usec: 0,
920 },
921 content: Vec::new(),
922 frame_count: 1,
923 }
924 }
925
926 fn parsed_with_address(address: &str) -> ParsedSipMessage {
927 let mut msg = make_parsed(
928 SipMessageType::Request {
929 method: "OPTIONS".into(),
930 uri: "sip:host".into(),
931 },
932 vec![],
933 b"",
934 );
935 msg.address = address.into();
936 msg
937 }
938
939 #[test]
940 fn socket_addr_ipv4() {
941 let addr = make_frame("10.0.0.1:5060").socket_addr().unwrap();
942 assert!(addr.is_ipv4());
943 assert_eq!(addr.port(), 5060);
944 assert_eq!(addr.ip().to_string(), "10.0.0.1");
945 }
946
947 #[test]
948 fn socket_addr_ipv6_bracketed() {
949 let addr = make_message("[2001:db8::1]:5061").socket_addr().unwrap();
950 assert!(addr.is_ipv6());
951 assert_eq!(addr.port(), 5061);
952 assert_eq!(addr.ip().to_string(), "2001:db8::1");
953 }
954
955 #[test]
956 fn socket_addr_ipv4_bracketed() {
957 let addr = make_frame("[198.51.100.7]:5060").socket_addr().unwrap();
958 assert!(addr.is_ipv4());
959 assert_eq!(addr.port(), 5060);
960 assert_eq!(addr.ip().to_string(), "198.51.100.7");
961 }
962
963 #[test]
964 fn socket_addr_on_parsed_message() {
965 let addr = parsed_with_address("192.0.2.4:5080").socket_addr().unwrap();
966 assert_eq!(addr.port(), 5080);
967 }
968
969 #[test]
970 fn socket_addr_rejects_non_addresses() {
971 for bad in [
972 "345.678.987.654:5060",
973 "10.0.0.1",
974 "host.example.test:5060",
975 "2001:db8::1:5060",
976 "",
977 ] {
978 assert!(
979 make_frame(bad).socket_addr().is_none(),
980 "should not parse: {bad}"
981 );
982 assert!(make_message(bad).socket_addr().is_none());
983 assert!(parsed_with_address(bad).socket_addr().is_none());
984 }
985 }
986
987 #[test]
988 fn to_bytes_request_no_body() {
989 let msg = make_parsed(
990 SipMessageType::Request {
991 method: "OPTIONS".into(),
992 uri: "sip:host".into(),
993 },
994 vec![("Call-ID", "test")],
995 b"",
996 );
997 let bytes = msg.to_bytes();
998 let text = String::from_utf8(bytes).unwrap();
999 assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
1000 assert!(text.contains("Call-ID: test\r\n"));
1001 assert!(text.ends_with("\r\n\r\n"));
1002 }
1003
1004 #[test]
1005 fn to_bytes_request_with_body() {
1006 let body = b"v=0\r\ns=-\r\n";
1007 let msg = make_parsed(
1008 SipMessageType::Request {
1009 method: "INVITE".into(),
1010 uri: "sip:host".into(),
1011 },
1012 vec![("Call-ID", "test")],
1013 body,
1014 );
1015 let bytes = msg.to_bytes();
1016 assert!(bytes.ends_with(body));
1017 }
1018
1019 #[test]
1020 fn to_bytes_response() {
1021 let msg = make_parsed(
1022 SipMessageType::Response {
1023 code: 200,
1024 reason: "OK".into(),
1025 },
1026 vec![("Call-ID", "resp-test")],
1027 b"",
1028 );
1029 let bytes = msg.to_bytes();
1030 let text = String::from_utf8(bytes).unwrap();
1031 assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
1032 }
1033
1034 #[test]
1035 fn body_data_valid_utf8() {
1036 let msg = make_parsed(
1037 SipMessageType::Request {
1038 method: "MESSAGE".into(),
1039 uri: "sip:host".into(),
1040 },
1041 vec![],
1042 b"hello world",
1043 );
1044 assert_eq!(&*msg.body_data(), "hello world");
1045 }
1046
1047 #[test]
1048 fn body_data_empty() {
1049 let msg = make_parsed(
1050 SipMessageType::Request {
1051 method: "OPTIONS".into(),
1052 uri: "sip:host".into(),
1053 },
1054 vec![],
1055 b"",
1056 );
1057 assert_eq!(&*msg.body_data(), "");
1058 }
1059
1060 #[test]
1061 fn body_data_binary() {
1062 let msg = make_parsed(
1063 SipMessageType::Request {
1064 method: "MESSAGE".into(),
1065 uri: "sip:host".into(),
1066 },
1067 vec![],
1068 &[0xFF, 0xFE],
1069 );
1070 assert!(msg.body_data().contains('\u{FFFD}'));
1071 }
1072
1073 #[test]
1074 fn body_data_preserves_json_escapes() {
1075 let raw = br#"{"key":"value\nwith\\escapes"}"#;
1076 let msg = make_parsed(
1077 SipMessageType::Request {
1078 method: "NOTIFY".into(),
1079 uri: "sip:host".into(),
1080 },
1081 vec![("Content-Type", "application/json")],
1082 raw,
1083 );
1084 assert_eq!(
1085 msg.body_data().as_ref(),
1086 r#"{"key":"value\nwith\\escapes"}"#,
1087 "body_data() must preserve raw escapes"
1088 );
1089 }
1090}