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 pub offset: u64,
554}
555
556impl Frame {
557 pub fn meta(&self) -> FrameMeta<'_> {
559 FrameMeta {
560 direction: self.direction,
561 transport: self.transport,
562 address: &self.address,
563 timestamp: self.timestamp,
564 }
565 }
566
567 pub fn socket_addr(&self) -> Option<SocketAddr> {
571 self.meta().socket_addr()
572 }
573}
574
575#[derive(Debug, Clone)]
580pub struct SipMessage {
581 pub direction: Direction,
583 pub transport: Transport,
585 pub address: String,
587 pub timestamp: Timestamp,
589 pub content: Vec<u8>,
591 pub frame_count: usize,
593 pub offset: u64,
596}
597
598impl SipMessage {
599 pub fn meta(&self) -> FrameMeta<'_> {
601 FrameMeta {
602 direction: self.direction,
603 transport: self.transport,
604 address: &self.address,
605 timestamp: self.timestamp,
606 }
607 }
608
609 pub fn socket_addr(&self) -> Option<SocketAddr> {
613 self.meta().socket_addr()
614 }
615}
616
617#[derive(Debug, Clone, PartialEq, Eq)]
619pub enum SipMessageType {
620 Request {
622 method: String,
624 uri: String,
626 },
627 Response {
629 code: u16,
631 reason: String,
633 },
634}
635
636impl fmt::Display for SipMessageType {
637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638 match self {
639 SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
640 SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
641 }
642 }
643}
644
645impl SipMessageType {
646 pub fn summary(&self) -> Cow<'_, str> {
648 match self {
649 SipMessageType::Request { method, .. } => Cow::Borrowed(method),
650 SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
651 }
652 }
653}
654
655#[derive(Debug, Clone, Default, PartialEq, Eq)]
658pub struct Headers(Vec<(String, String)>);
659
660impl Headers {
661 pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
664 self.0
665 .iter()
666 .filter(move |(k, _)| k.eq_ignore_ascii_case(name))
667 .map(|(_, v)| v.as_str())
668 }
669
670 pub fn value(&self, name: &str) -> Option<&str> {
674 self.0
675 .iter()
676 .find(|(k, _)| k.eq_ignore_ascii_case(name))
677 .map(|(_, v)| v.as_str())
678 }
679}
680
681impl std::ops::Deref for Headers {
682 type Target = [(String, String)];
683
684 fn deref(&self) -> &Self::Target {
685 &self.0
686 }
687}
688
689impl std::ops::DerefMut for Headers {
690 fn deref_mut(&mut self) -> &mut Self::Target {
691 &mut self.0
692 }
693}
694
695impl From<Vec<(String, String)>> for Headers {
696 fn from(headers: Vec<(String, String)>) -> Self {
697 Headers(headers)
698 }
699}
700
701impl FromIterator<(String, String)> for Headers {
702 fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
703 Headers(iter.into_iter().collect())
704 }
705}
706
707impl<'a> IntoIterator for &'a Headers {
708 type Item = &'a (String, String);
709 type IntoIter = std::slice::Iter<'a, (String, String)>;
710
711 fn into_iter(self) -> Self::IntoIter {
712 self.0.iter()
713 }
714}
715
716#[derive(Debug, Clone)]
723pub struct ParsedSipMessage {
724 pub direction: Direction,
726 pub transport: Transport,
728 pub address: String,
730 pub timestamp: Timestamp,
732 pub message_type: SipMessageType,
734 pub headers: Headers,
736 pub body: Vec<u8>,
738 pub frame_count: usize,
740 pub offset: u64,
743}
744
745#[derive(Debug, Clone, PartialEq, Eq)]
752pub struct SipFragment {
753 pub message_type: Option<SipMessageType>,
755 pub headers: Headers,
757 pub body: Vec<u8>,
759}
760
761impl SipFragment {
762 pub fn header_value(&self, name: &str) -> Option<&str> {
764 self.headers.value(name)
765 }
766
767 pub fn content_type(&self) -> Option<&str> {
770 value_or_compact(&self.headers, "Content-Type")
771 }
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
776pub struct MimePart {
777 pub headers: Headers,
779 pub body: Vec<u8>,
781}
782
783impl MimePart {
784 pub fn content_type(&self) -> Option<&str> {
787 value_or_compact(&self.headers, "Content-Type")
788 }
789
790 pub fn header_value(&self, name: &str) -> Option<&str> {
792 self.headers.value(name)
793 }
794
795 pub fn content_id(&self) -> Option<&str> {
797 self.header_value("Content-ID")
798 }
799
800 pub fn content_disposition(&self) -> Option<&str> {
802 self.header_value("Content-Disposition")
803 }
804
805 pub fn content_transfer_encoding(&self) -> Option<&str> {
809 self.header_value("Content-Transfer-Encoding")
810 }
811}
812
813impl ParsedSipMessage {
814 pub fn meta(&self) -> FrameMeta<'_> {
816 FrameMeta {
817 direction: self.direction,
818 transport: self.transport,
819 address: &self.address,
820 timestamp: self.timestamp,
821 }
822 }
823
824 pub fn socket_addr(&self) -> Option<SocketAddr> {
828 self.meta().socket_addr()
829 }
830
831 pub fn call_id(&self) -> Option<&str> {
834 value_or_compact(&self.headers, "Call-ID")
835 }
836
837 pub fn content_type(&self) -> Option<&str> {
840 value_or_compact(&self.headers, "Content-Type")
841 }
842
843 pub fn content_length(&self) -> Option<usize> {
846 value_or_compact(&self.headers, "Content-Length").and_then(|v| v.trim().parse().ok())
847 }
848
849 pub fn cseq(&self) -> Option<&str> {
851 self.header_value("CSeq")
852 }
853
854 pub fn method(&self) -> Option<&str> {
857 match &self.message_type {
858 SipMessageType::Request { method, .. } => Some(method),
859 SipMessageType::Response { .. } => {
860 self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
861 }
862 }
863 }
864
865 pub fn body_data(&self) -> Cow<'_, str> {
868 String::from_utf8_lossy(&self.body)
869 }
870
871 pub fn to_bytes(&self) -> Vec<u8> {
873 let mut out = Vec::new();
874 match &self.message_type {
875 SipMessageType::Request { method, uri } => {
876 out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
877 }
878 SipMessageType::Response { code, reason } => {
879 out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
880 }
881 }
882 for (name, value) in &self.headers {
883 out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
884 }
885 out.extend_from_slice(b"\r\n");
886 out.extend_from_slice(&self.body);
887 out
888 }
889
890 pub fn header_value(&self, name: &str) -> Option<&str> {
893 self.headers.value(name)
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900
901 fn make_parsed(
902 msg_type: SipMessageType,
903 headers: Vec<(&str, &str)>,
904 body: &[u8],
905 ) -> ParsedSipMessage {
906 ParsedSipMessage {
907 direction: Direction::Recv,
908 transport: Transport::Tcp,
909 address: "10.0.0.1:5060".into(),
910 timestamp: Timestamp::TimeOnly {
911 hour: 12,
912 min: 0,
913 sec: 0,
914 usec: 0,
915 },
916 message_type: msg_type,
917 headers: Headers(
918 headers
919 .iter()
920 .map(|(k, v)| (k.to_string(), v.to_string()))
921 .collect(),
922 ),
923 body: body.to_vec(),
924 frame_count: 1,
925 offset: 0,
926 }
927 }
928
929 fn make_frame(address: &str) -> Frame {
930 Frame {
931 direction: Direction::Recv,
932 byte_count: 0,
933 transport: Transport::Tcp,
934 address: address.into(),
935 timestamp: Timestamp::TimeOnly {
936 hour: 0,
937 min: 0,
938 sec: 0,
939 usec: 0,
940 },
941 content: Vec::new(),
942 offset: 0,
943 }
944 }
945
946 fn make_message(address: &str) -> SipMessage {
947 SipMessage {
948 direction: Direction::Recv,
949 transport: Transport::Tcp,
950 address: address.into(),
951 timestamp: Timestamp::TimeOnly {
952 hour: 0,
953 min: 0,
954 sec: 0,
955 usec: 0,
956 },
957 content: Vec::new(),
958 frame_count: 1,
959 offset: 0,
960 }
961 }
962
963 fn parsed_with_address(address: &str) -> ParsedSipMessage {
964 let mut msg = make_parsed(
965 SipMessageType::Request {
966 method: "OPTIONS".into(),
967 uri: "sip:host".into(),
968 },
969 vec![],
970 b"",
971 );
972 msg.address = address.into();
973 msg
974 }
975
976 #[test]
977 fn socket_addr_ipv4() {
978 let addr = make_frame("10.0.0.1:5060").socket_addr().unwrap();
979 assert!(addr.is_ipv4());
980 assert_eq!(addr.port(), 5060);
981 assert_eq!(addr.ip().to_string(), "10.0.0.1");
982 }
983
984 #[test]
985 fn socket_addr_ipv6_bracketed() {
986 let addr = make_message("[2001:db8::1]:5061").socket_addr().unwrap();
987 assert!(addr.is_ipv6());
988 assert_eq!(addr.port(), 5061);
989 assert_eq!(addr.ip().to_string(), "2001:db8::1");
990 }
991
992 #[test]
993 fn socket_addr_ipv4_bracketed() {
994 let addr = make_frame("[198.51.100.7]:5060").socket_addr().unwrap();
995 assert!(addr.is_ipv4());
996 assert_eq!(addr.port(), 5060);
997 assert_eq!(addr.ip().to_string(), "198.51.100.7");
998 }
999
1000 #[test]
1001 fn socket_addr_on_parsed_message() {
1002 let addr = parsed_with_address("192.0.2.4:5080").socket_addr().unwrap();
1003 assert_eq!(addr.port(), 5080);
1004 }
1005
1006 #[test]
1007 fn socket_addr_rejects_non_addresses() {
1008 for bad in [
1009 "345.678.987.654:5060",
1010 "10.0.0.1",
1011 "host.example.test:5060",
1012 "2001:db8::1:5060",
1013 "",
1014 ] {
1015 assert!(
1016 make_frame(bad).socket_addr().is_none(),
1017 "should not parse: {bad}"
1018 );
1019 assert!(make_message(bad).socket_addr().is_none());
1020 assert!(parsed_with_address(bad).socket_addr().is_none());
1021 }
1022 }
1023
1024 #[test]
1025 fn to_bytes_request_no_body() {
1026 let msg = make_parsed(
1027 SipMessageType::Request {
1028 method: "OPTIONS".into(),
1029 uri: "sip:host".into(),
1030 },
1031 vec![("Call-ID", "test")],
1032 b"",
1033 );
1034 let bytes = msg.to_bytes();
1035 let text = String::from_utf8(bytes).unwrap();
1036 assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
1037 assert!(text.contains("Call-ID: test\r\n"));
1038 assert!(text.ends_with("\r\n\r\n"));
1039 }
1040
1041 #[test]
1042 fn to_bytes_request_with_body() {
1043 let body = b"v=0\r\ns=-\r\n";
1044 let msg = make_parsed(
1045 SipMessageType::Request {
1046 method: "INVITE".into(),
1047 uri: "sip:host".into(),
1048 },
1049 vec![("Call-ID", "test")],
1050 body,
1051 );
1052 let bytes = msg.to_bytes();
1053 assert!(bytes.ends_with(body));
1054 }
1055
1056 #[test]
1057 fn to_bytes_response() {
1058 let msg = make_parsed(
1059 SipMessageType::Response {
1060 code: 200,
1061 reason: "OK".into(),
1062 },
1063 vec![("Call-ID", "resp-test")],
1064 b"",
1065 );
1066 let bytes = msg.to_bytes();
1067 let text = String::from_utf8(bytes).unwrap();
1068 assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
1069 }
1070
1071 #[test]
1072 fn body_data_valid_utf8() {
1073 let msg = make_parsed(
1074 SipMessageType::Request {
1075 method: "MESSAGE".into(),
1076 uri: "sip:host".into(),
1077 },
1078 vec![],
1079 b"hello world",
1080 );
1081 assert_eq!(&*msg.body_data(), "hello world");
1082 }
1083
1084 #[test]
1085 fn body_data_empty() {
1086 let msg = make_parsed(
1087 SipMessageType::Request {
1088 method: "OPTIONS".into(),
1089 uri: "sip:host".into(),
1090 },
1091 vec![],
1092 b"",
1093 );
1094 assert_eq!(&*msg.body_data(), "");
1095 }
1096
1097 #[test]
1098 fn body_data_binary() {
1099 let msg = make_parsed(
1100 SipMessageType::Request {
1101 method: "MESSAGE".into(),
1102 uri: "sip:host".into(),
1103 },
1104 vec![],
1105 &[0xFF, 0xFE],
1106 );
1107 assert!(msg.body_data().contains('\u{FFFD}'));
1108 }
1109
1110 #[test]
1111 fn body_data_preserves_json_escapes() {
1112 let raw = br#"{"key":"value\nwith\\escapes"}"#;
1113 let msg = make_parsed(
1114 SipMessageType::Request {
1115 method: "NOTIFY".into(),
1116 uri: "sip:host".into(),
1117 },
1118 vec![("Content-Type", "application/json")],
1119 raw,
1120 );
1121 assert_eq!(
1122 msg.body_data().as_ref(),
1123 r#"{"key":"value\nwith\\escapes"}"#,
1124 "body_data() must preserve raw escapes"
1125 );
1126 }
1127}