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) stale_evictions: u64,
121 pub(crate) stale_evicted_bytes: u64,
122 pub(crate) non_sip_prefixes: u64,
123 pub(crate) non_sip_prefix_bytes: u64,
124 pub(crate) unparsed_regions: Vec<UnparsedRegion>,
125}
126
127impl ParseStats {
128 pub fn bytes_read(&self) -> u64 {
130 self.bytes_read
131 }
132
133 pub fn bytes_skipped(&self) -> u64 {
135 self.bytes_skipped
136 }
137
138 pub fn incomplete_frames(&self) -> u64 {
141 self.incomplete_frames
142 }
143
144 pub fn incomplete_frame_bytes(&self) -> u64 {
147 self.incomplete_frame_bytes
148 }
149
150 pub fn stale_evictions(&self) -> u64 {
154 self.stale_evictions
155 }
156
157 pub fn stale_evicted_bytes(&self) -> u64 {
159 self.stale_evicted_bytes
160 }
161
162 pub fn non_sip_prefixes(&self) -> u64 {
164 self.non_sip_prefixes
165 }
166
167 pub fn non_sip_prefix_bytes(&self) -> u64 {
169 self.non_sip_prefix_bytes
170 }
171
172 pub fn unparsed_regions(&self) -> &[UnparsedRegion] {
175 &self.unparsed_regions
176 }
177
178 pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
180 std::mem::take(&mut self.unparsed_regions)
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct UnknownKeyword(String);
188
189impl UnknownKeyword {
190 pub fn as_str(&self) -> &str {
192 &self.0
193 }
194}
195
196impl fmt::Display for UnknownKeyword {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(f, "unknown keyword: {}", self.0)
199 }
200}
201
202impl std::error::Error for UnknownKeyword {}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206pub enum Direction {
207 Recv,
209 Sent,
211}
212
213impl fmt::Display for Direction {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 f.write_str(self.as_str())
216 }
217}
218
219impl std::str::FromStr for Direction {
220 type Err = UnknownKeyword;
221
222 fn from_str(s: &str) -> Result<Self, Self::Err> {
224 for candidate in [Direction::Recv, Direction::Sent] {
225 if s.eq_ignore_ascii_case(candidate.as_str()) {
226 return Ok(candidate);
227 }
228 }
229 Err(UnknownKeyword(s.to_string()))
230 }
231}
232
233impl Direction {
234 pub fn as_str(&self) -> &'static str {
236 match self {
237 Direction::Recv => "recv",
238 Direction::Sent => "sent",
239 }
240 }
241
242 pub fn preposition(&self) -> &'static str {
244 match self {
245 Direction::Recv => "from",
246 Direction::Sent => "to",
247 }
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
253pub enum Transport {
254 Tcp,
256 Udp,
258 Tls,
260 Wss,
262}
263
264impl Transport {
265 pub fn as_str(&self) -> &'static str {
267 match self {
268 Transport::Tcp => "tcp",
269 Transport::Udp => "udp",
270 Transport::Tls => "tls",
271 Transport::Wss => "wss",
272 }
273 }
274}
275
276impl fmt::Display for Transport {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 f.write_str(self.as_str())
279 }
280}
281
282impl std::str::FromStr for Transport {
283 type Err = UnknownKeyword;
284
285 fn from_str(s: &str) -> Result<Self, Self::Err> {
287 for candidate in [
288 Transport::Tcp,
289 Transport::Udp,
290 Transport::Tls,
291 Transport::Wss,
292 ] {
293 if s.eq_ignore_ascii_case(candidate.as_str()) {
294 return Ok(candidate);
295 }
296 }
297 Err(UnknownKeyword(s.to_string()))
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum Timestamp {
307 TimeOnly {
309 hour: u8,
311 min: u8,
313 sec: u8,
315 usec: u32,
317 },
318 DateTime {
320 year: u16,
322 month: u8,
324 day: u8,
326 hour: u8,
328 min: u8,
330 sec: u8,
332 usec: u32,
334 },
335}
336
337impl Timestamp {
338 pub fn time_of_day_secs(&self) -> u32 {
340 let (h, m, s) = match self {
341 Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
342 Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
343 };
344 h as u32 * 3600 + m as u32 * 60 + s as u32
345 }
346
347 pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
350 match self {
351 Timestamp::TimeOnly {
352 hour,
353 min,
354 sec,
355 usec,
356 } => (0, 0, 0, *hour, *min, *sec, *usec),
357 Timestamp::DateTime {
358 year,
359 month,
360 day,
361 hour,
362 min,
363 sec,
364 usec,
365 } => (*year, *month, *day, *hour, *min, *sec, *usec),
366 }
367 }
368}
369
370pub(crate) fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
373 let y = if m <= 2 { y - 1 } else { y };
374 let era = if y >= 0 { y } else { y - 399 } / 400;
375 let yoe = (y - era * 400) as u64;
376 let m_adj = if m > 2 { m as i64 - 3 } else { m as i64 + 9 } as u64;
377 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
378 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
379 era * 146097 + doe as i64 - 719468
380}
381
382#[derive(Debug, Default, Clone)]
392pub struct StaleClock {
393 day: u32,
394 last_time_secs: u32,
395 now: u64,
396 last_sweep: u64,
397 dated: Option<bool>,
398 reset: bool,
399}
400
401impl StaleClock {
402 pub const TIMEOUT_SECS: u64 = 7200;
406
407 pub fn new() -> Self {
409 Self::default()
410 }
411
412 pub fn observe(&mut self, timestamp: Timestamp) -> u64 {
414 let dated = matches!(timestamp, Timestamp::DateTime { .. });
415 if self.dated != Some(dated) {
416 self.dated = Some(dated);
417 self.day = 0;
418 self.last_time_secs = 0;
419 self.reset = true;
420 }
421
422 let time_secs = timestamp.time_of_day_secs();
423 self.now = match timestamp {
424 Timestamp::DateTime {
425 year, month, day, ..
426 } => {
427 let days = days_from_civil(year as i64, month as u32, day as u32).max(0) as u64;
428 days * 86400 + time_secs as u64
429 }
430 Timestamp::TimeOnly { .. } => {
431 if time_secs < self.last_time_secs && self.last_time_secs - time_secs > 43200 {
432 self.day += 1;
433 }
434 self.day as u64 * 86400 + time_secs as u64
435 }
436 };
437 self.last_time_secs = time_secs;
438 self.now
439 }
440
441 pub fn now(&self) -> u64 {
443 self.now
444 }
445
446 pub fn sweep_due(&mut self) -> bool {
450 if self.reset {
451 self.reset = false;
452 self.last_sweep = self.now;
453 return false;
454 }
455 if self.now.saturating_sub(self.last_sweep) >= Self::TIMEOUT_SECS {
456 self.last_sweep = self.now;
457 return true;
458 }
459 false
460 }
461
462 pub fn is_stale(&self, last_seen: u64) -> bool {
464 self.now.saturating_sub(last_seen) > Self::TIMEOUT_SECS
465 }
466}
467
468impl fmt::Display for Timestamp {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 match self {
471 Timestamp::TimeOnly {
472 hour,
473 min,
474 sec,
475 usec,
476 } => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
477 Timestamp::DateTime {
478 year,
479 month,
480 day,
481 hour,
482 min,
483 sec,
484 usec,
485 } => write!(
486 f,
487 "{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
488 ),
489 }
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub struct FrameMeta<'a> {
500 pub direction: Direction,
502 pub transport: Transport,
504 pub address: &'a str,
506 pub timestamp: Timestamp,
508}
509
510impl FrameMeta<'_> {
511 pub fn socket_addr(&self) -> Option<SocketAddr> {
515 parse_socket_addr(self.address)
516 }
517}
518
519impl fmt::Display for FrameMeta<'_> {
520 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521 write!(
522 f,
523 "{} {} {}/{} at {}",
524 self.direction,
525 self.direction.preposition(),
526 self.transport,
527 self.address,
528 self.timestamp
529 )
530 }
531}
532
533#[derive(Debug, Clone)]
539pub struct Frame {
540 pub direction: Direction,
542 pub byte_count: usize,
544 pub transport: Transport,
546 pub address: String,
548 pub timestamp: Timestamp,
550 pub content: Vec<u8>,
552}
553
554impl Frame {
555 pub fn meta(&self) -> FrameMeta<'_> {
557 FrameMeta {
558 direction: self.direction,
559 transport: self.transport,
560 address: &self.address,
561 timestamp: self.timestamp,
562 }
563 }
564
565 pub fn socket_addr(&self) -> Option<SocketAddr> {
569 self.meta().socket_addr()
570 }
571}
572
573#[derive(Debug, Clone)]
578pub struct SipMessage {
579 pub direction: Direction,
581 pub transport: Transport,
583 pub address: String,
585 pub timestamp: Timestamp,
587 pub content: Vec<u8>,
589 pub frame_count: usize,
591}
592
593impl SipMessage {
594 pub fn meta(&self) -> FrameMeta<'_> {
596 FrameMeta {
597 direction: self.direction,
598 transport: self.transport,
599 address: &self.address,
600 timestamp: self.timestamp,
601 }
602 }
603
604 pub fn socket_addr(&self) -> Option<SocketAddr> {
608 self.meta().socket_addr()
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
614pub enum SipMessageType {
615 Request {
617 method: String,
619 uri: String,
621 },
622 Response {
624 code: u16,
626 reason: String,
628 },
629}
630
631impl fmt::Display for SipMessageType {
632 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633 match self {
634 SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
635 SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
636 }
637 }
638}
639
640impl SipMessageType {
641 pub fn summary(&self) -> Cow<'_, str> {
643 match self {
644 SipMessageType::Request { method, .. } => Cow::Borrowed(method),
645 SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
646 }
647 }
648}
649
650#[derive(Debug, Clone, Default, PartialEq, Eq)]
653pub struct Headers(Vec<(String, String)>);
654
655impl Headers {
656 pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
659 self.0
660 .iter()
661 .filter(move |(k, _)| k.eq_ignore_ascii_case(name))
662 .map(|(_, v)| v.as_str())
663 }
664
665 pub fn value(&self, name: &str) -> Option<&str> {
669 self.0
670 .iter()
671 .find(|(k, _)| k.eq_ignore_ascii_case(name))
672 .map(|(_, v)| v.as_str())
673 }
674}
675
676impl std::ops::Deref for Headers {
677 type Target = [(String, String)];
678
679 fn deref(&self) -> &Self::Target {
680 &self.0
681 }
682}
683
684impl std::ops::DerefMut for Headers {
685 fn deref_mut(&mut self) -> &mut Self::Target {
686 &mut self.0
687 }
688}
689
690impl From<Vec<(String, String)>> for Headers {
691 fn from(headers: Vec<(String, String)>) -> Self {
692 Headers(headers)
693 }
694}
695
696impl FromIterator<(String, String)> for Headers {
697 fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
698 Headers(iter.into_iter().collect())
699 }
700}
701
702impl<'a> IntoIterator for &'a Headers {
703 type Item = &'a (String, String);
704 type IntoIter = std::slice::Iter<'a, (String, String)>;
705
706 fn into_iter(self) -> Self::IntoIter {
707 self.0.iter()
708 }
709}
710
711#[derive(Debug, Clone)]
718pub struct ParsedSipMessage {
719 pub direction: Direction,
721 pub transport: Transport,
723 pub address: String,
725 pub timestamp: Timestamp,
727 pub message_type: SipMessageType,
729 pub headers: Headers,
731 pub body: Vec<u8>,
733 pub frame_count: usize,
735}
736
737#[derive(Debug, Clone, PartialEq, Eq)]
744pub struct SipFragment {
745 pub message_type: Option<SipMessageType>,
747 pub headers: Headers,
749 pub body: Vec<u8>,
751}
752
753impl SipFragment {
754 pub fn header_value(&self, name: &str) -> Option<&str> {
756 self.headers.value(name)
757 }
758
759 pub fn content_type(&self) -> Option<&str> {
762 value_or_compact(&self.headers, "Content-Type")
763 }
764}
765
766#[derive(Debug, Clone, PartialEq, Eq)]
768pub struct MimePart {
769 pub headers: Headers,
771 pub body: Vec<u8>,
773}
774
775impl MimePart {
776 pub fn content_type(&self) -> Option<&str> {
779 value_or_compact(&self.headers, "Content-Type")
780 }
781
782 pub fn header_value(&self, name: &str) -> Option<&str> {
784 self.headers.value(name)
785 }
786
787 pub fn content_id(&self) -> Option<&str> {
789 self.header_value("Content-ID")
790 }
791
792 pub fn content_disposition(&self) -> Option<&str> {
794 self.header_value("Content-Disposition")
795 }
796
797 pub fn content_transfer_encoding(&self) -> Option<&str> {
801 self.header_value("Content-Transfer-Encoding")
802 }
803}
804
805impl ParsedSipMessage {
806 pub fn meta(&self) -> FrameMeta<'_> {
808 FrameMeta {
809 direction: self.direction,
810 transport: self.transport,
811 address: &self.address,
812 timestamp: self.timestamp,
813 }
814 }
815
816 pub fn socket_addr(&self) -> Option<SocketAddr> {
820 self.meta().socket_addr()
821 }
822
823 pub fn call_id(&self) -> Option<&str> {
826 value_or_compact(&self.headers, "Call-ID")
827 }
828
829 pub fn content_type(&self) -> Option<&str> {
832 value_or_compact(&self.headers, "Content-Type")
833 }
834
835 pub fn content_length(&self) -> Option<usize> {
838 value_or_compact(&self.headers, "Content-Length").and_then(|v| v.trim().parse().ok())
839 }
840
841 pub fn cseq(&self) -> Option<&str> {
843 self.header_value("CSeq")
844 }
845
846 pub fn method(&self) -> Option<&str> {
849 match &self.message_type {
850 SipMessageType::Request { method, .. } => Some(method),
851 SipMessageType::Response { .. } => {
852 self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
853 }
854 }
855 }
856
857 pub fn body_data(&self) -> Cow<'_, str> {
860 String::from_utf8_lossy(&self.body)
861 }
862
863 pub fn to_bytes(&self) -> Vec<u8> {
865 let mut out = Vec::new();
866 match &self.message_type {
867 SipMessageType::Request { method, uri } => {
868 out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
869 }
870 SipMessageType::Response { code, reason } => {
871 out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
872 }
873 }
874 for (name, value) in &self.headers {
875 out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
876 }
877 out.extend_from_slice(b"\r\n");
878 out.extend_from_slice(&self.body);
879 out
880 }
881
882 pub fn header_value(&self, name: &str) -> Option<&str> {
885 self.headers.value(name)
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 fn make_parsed(
894 msg_type: SipMessageType,
895 headers: Vec<(&str, &str)>,
896 body: &[u8],
897 ) -> ParsedSipMessage {
898 ParsedSipMessage {
899 direction: Direction::Recv,
900 transport: Transport::Tcp,
901 address: "10.0.0.1:5060".into(),
902 timestamp: Timestamp::TimeOnly {
903 hour: 12,
904 min: 0,
905 sec: 0,
906 usec: 0,
907 },
908 message_type: msg_type,
909 headers: Headers(
910 headers
911 .iter()
912 .map(|(k, v)| (k.to_string(), v.to_string()))
913 .collect(),
914 ),
915 body: body.to_vec(),
916 frame_count: 1,
917 }
918 }
919
920 fn make_frame(address: &str) -> Frame {
921 Frame {
922 direction: Direction::Recv,
923 byte_count: 0,
924 transport: Transport::Tcp,
925 address: address.into(),
926 timestamp: Timestamp::TimeOnly {
927 hour: 0,
928 min: 0,
929 sec: 0,
930 usec: 0,
931 },
932 content: Vec::new(),
933 }
934 }
935
936 fn make_message(address: &str) -> SipMessage {
937 SipMessage {
938 direction: Direction::Recv,
939 transport: Transport::Tcp,
940 address: address.into(),
941 timestamp: Timestamp::TimeOnly {
942 hour: 0,
943 min: 0,
944 sec: 0,
945 usec: 0,
946 },
947 content: Vec::new(),
948 frame_count: 1,
949 }
950 }
951
952 fn parsed_with_address(address: &str) -> ParsedSipMessage {
953 let mut msg = make_parsed(
954 SipMessageType::Request {
955 method: "OPTIONS".into(),
956 uri: "sip:host".into(),
957 },
958 vec![],
959 b"",
960 );
961 msg.address = address.into();
962 msg
963 }
964
965 #[test]
966 fn socket_addr_ipv4() {
967 let addr = make_frame("10.0.0.1:5060").socket_addr().unwrap();
968 assert!(addr.is_ipv4());
969 assert_eq!(addr.port(), 5060);
970 assert_eq!(addr.ip().to_string(), "10.0.0.1");
971 }
972
973 #[test]
974 fn socket_addr_ipv6_bracketed() {
975 let addr = make_message("[2001:db8::1]:5061").socket_addr().unwrap();
976 assert!(addr.is_ipv6());
977 assert_eq!(addr.port(), 5061);
978 assert_eq!(addr.ip().to_string(), "2001:db8::1");
979 }
980
981 #[test]
982 fn socket_addr_ipv4_bracketed() {
983 let addr = make_frame("[198.51.100.7]:5060").socket_addr().unwrap();
984 assert!(addr.is_ipv4());
985 assert_eq!(addr.port(), 5060);
986 assert_eq!(addr.ip().to_string(), "198.51.100.7");
987 }
988
989 #[test]
990 fn socket_addr_on_parsed_message() {
991 let addr = parsed_with_address("192.0.2.4:5080").socket_addr().unwrap();
992 assert_eq!(addr.port(), 5080);
993 }
994
995 #[test]
996 fn socket_addr_rejects_non_addresses() {
997 for bad in [
998 "345.678.987.654:5060",
999 "10.0.0.1",
1000 "host.example.test:5060",
1001 "2001:db8::1:5060",
1002 "",
1003 ] {
1004 assert!(
1005 make_frame(bad).socket_addr().is_none(),
1006 "should not parse: {bad}"
1007 );
1008 assert!(make_message(bad).socket_addr().is_none());
1009 assert!(parsed_with_address(bad).socket_addr().is_none());
1010 }
1011 }
1012
1013 #[test]
1014 fn to_bytes_request_no_body() {
1015 let msg = make_parsed(
1016 SipMessageType::Request {
1017 method: "OPTIONS".into(),
1018 uri: "sip:host".into(),
1019 },
1020 vec![("Call-ID", "test")],
1021 b"",
1022 );
1023 let bytes = msg.to_bytes();
1024 let text = String::from_utf8(bytes).unwrap();
1025 assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
1026 assert!(text.contains("Call-ID: test\r\n"));
1027 assert!(text.ends_with("\r\n\r\n"));
1028 }
1029
1030 #[test]
1031 fn to_bytes_request_with_body() {
1032 let body = b"v=0\r\ns=-\r\n";
1033 let msg = make_parsed(
1034 SipMessageType::Request {
1035 method: "INVITE".into(),
1036 uri: "sip:host".into(),
1037 },
1038 vec![("Call-ID", "test")],
1039 body,
1040 );
1041 let bytes = msg.to_bytes();
1042 assert!(bytes.ends_with(body));
1043 }
1044
1045 #[test]
1046 fn to_bytes_response() {
1047 let msg = make_parsed(
1048 SipMessageType::Response {
1049 code: 200,
1050 reason: "OK".into(),
1051 },
1052 vec![("Call-ID", "resp-test")],
1053 b"",
1054 );
1055 let bytes = msg.to_bytes();
1056 let text = String::from_utf8(bytes).unwrap();
1057 assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
1058 }
1059
1060 #[test]
1061 fn body_data_valid_utf8() {
1062 let msg = make_parsed(
1063 SipMessageType::Request {
1064 method: "MESSAGE".into(),
1065 uri: "sip:host".into(),
1066 },
1067 vec![],
1068 b"hello world",
1069 );
1070 assert_eq!(&*msg.body_data(), "hello world");
1071 }
1072
1073 #[test]
1074 fn body_data_empty() {
1075 let msg = make_parsed(
1076 SipMessageType::Request {
1077 method: "OPTIONS".into(),
1078 uri: "sip:host".into(),
1079 },
1080 vec![],
1081 b"",
1082 );
1083 assert_eq!(&*msg.body_data(), "");
1084 }
1085
1086 #[test]
1087 fn body_data_binary() {
1088 let msg = make_parsed(
1089 SipMessageType::Request {
1090 method: "MESSAGE".into(),
1091 uri: "sip:host".into(),
1092 },
1093 vec![],
1094 &[0xFF, 0xFE],
1095 );
1096 assert!(msg.body_data().contains('\u{FFFD}'));
1097 }
1098
1099 #[test]
1100 fn body_data_preserves_json_escapes() {
1101 let raw = br#"{"key":"value\nwith\\escapes"}"#;
1102 let msg = make_parsed(
1103 SipMessageType::Request {
1104 method: "NOTIFY".into(),
1105 uri: "sip:host".into(),
1106 },
1107 vec![("Content-Type", "application/json")],
1108 raw,
1109 );
1110 assert_eq!(
1111 msg.body_data().as_ref(),
1112 r#"{"key":"value\nwith\\escapes"}"#,
1113 "body_data() must preserve raw escapes"
1114 );
1115 }
1116}