Skip to main content

freeswitch_sofia_trace_parser/
types.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::net::SocketAddr;
4
5/// Canonical name of an RFC 3261 §7.3.3 compact form, `None` for any other
6/// header name.
7pub(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
14/// Value recorded under `name` or under the compact form that expands to it,
15/// preferring the full name wherever the message carries both.
16pub(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
32/// mod_sofia brackets IPv4 like IPv6 (`[198.51.100.7]:5060`); anything that
33/// isn't an ip:port shape yields `None` rather than a guess.
34fn 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/// Why a region of the input stream was not parsed into a frame.
43///
44/// Every byte in the input is either parsed or classified with one of these
45/// reasons, enabling byte-level coverage accounting via [`ParseStats`].
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum SkipReason {
49    /// Truncated frame at the start of a file, typically from logrotate
50    /// cutting mid-write. Capped at 65,537 bytes: the largest datagram plus
51    /// the two-byte boundary.
52    PartialFirstFrame,
53    /// Skip region exceeds 65,537 bytes, at file start or mid-stream,
54    /// indicating the input is not a dump file (e.g., compressed or binary
55    /// data).
56    OversizedFrame,
57    /// Unrecoverable bytes skipped between valid frames mid-stream.
58    MidStreamSkip,
59    /// Logrotate wrote a partial frame tail at the start of the new file.
60    /// Detected by the `\r\n\r\n\x0B\n` suffix pattern.
61    ReplayedFrame,
62    /// Frame at EOF with fewer content bytes than declared in the header.
63    IncompleteFrame,
64    /// Data starts with `recv`/`sent` but fails frame header parsing.
65    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/// Controls how much detail the parser records about unparsed regions.
82///
83/// Defaults to `CountOnly` for constant-memory operation. Higher levels
84/// allocate per-region and should only be enabled for diagnostics.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SkipTracking {
87    /// Track only `bytes_read` and `bytes_skipped` counters. No allocation.
88    CountOnly,
89    /// Record offset, length, and reason for each unparsed region.
90    TrackRegions,
91    /// Like `TrackRegions`, but also capture the skipped bytes themselves.
92    CaptureData,
93}
94
95/// A contiguous region of the input that was not parsed into a frame.
96#[derive(Debug, Clone)]
97#[non_exhaustive]
98pub struct UnparsedRegion {
99    /// Byte offset from the start of the input stream.
100    pub offset: u64,
101    /// Number of bytes in this region.
102    pub length: u64,
103    /// Why this region was skipped.
104    pub reason: SkipReason,
105    /// The raw bytes, populated only when [`SkipTracking::CaptureData`] is enabled.
106    pub data: Option<Vec<u8>>,
107}
108
109/// Byte-level parse coverage statistics.
110///
111/// Available from all three iterator levels via `stats()` or `parse_stats()`.
112/// Every byte consumed from the reader is accounted for as either parsed
113/// (`bytes_read - bytes_skipped`) or skipped (`bytes_skipped`).
114#[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    /// Total bytes consumed from the reader.
125    pub fn bytes_read(&self) -> u64 {
126        self.bytes_read
127    }
128
129    /// Bytes that were skipped (not parsed into frames).
130    pub fn bytes_skipped(&self) -> u64 {
131        self.bytes_skipped
132    }
133
134    /// Frames whose content ran out before `byte_count` at end of input.
135    /// Maintained in every [`SkipTracking`] mode.
136    pub fn incomplete_frames(&self) -> u64 {
137        self.incomplete_frames
138    }
139
140    /// Total shortfall of those frames. A caller stitching rotated files
141    /// together decides for itself whether a shortfall is a rotation cut.
142    pub fn incomplete_frame_bytes(&self) -> u64 {
143        self.incomplete_frame_bytes
144    }
145
146    /// Detailed unparsed region records. Only populated when
147    /// [`SkipTracking`] is `TrackRegions` or `CaptureData`.
148    pub fn unparsed_regions(&self) -> &[UnparsedRegion] {
149        &self.unparsed_regions
150    }
151
152    /// Take all accumulated unparsed regions, leaving the list empty.
153    pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
154        std::mem::take(&mut self.unparsed_regions)
155    }
156}
157
158/// The input named none of the keywords a [`Direction`] or [`Transport`]
159/// is written as in a frame header.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct UnknownKeyword(String);
162
163impl UnknownKeyword {
164    /// The rejected input.
165    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/// Whether a frame was received or sent by FreeSWITCH.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180pub enum Direction {
181    /// Received from the network.
182    Recv,
183    /// Sent to the network.
184    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    /// Accepts `recv` and `sent`, case-insensitively.
197    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    /// The keyword a frame header spells this direction with.
209    pub fn as_str(&self) -> &'static str {
210        match self {
211            Direction::Recv => "recv",
212            Direction::Sent => "sent",
213        }
214    }
215
216    /// Returns `"from"` for `Recv`, `"to"` for `Sent`.
217    pub fn preposition(&self) -> &'static str {
218        match self {
219            Direction::Recv => "from",
220            Direction::Sent => "to",
221        }
222    }
223}
224
225/// SIP transport protocol as reported in the frame header.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227pub enum Transport {
228    /// Transmission Control Protocol.
229    Tcp,
230    /// User Datagram Protocol.
231    Udp,
232    /// Transport Layer Security.
233    Tls,
234    /// WebSocket Secure (RFC 7118).
235    Wss,
236}
237
238impl Transport {
239    /// The keyword a frame header spells this transport with.
240    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    /// Accepts `tcp`, `udp`, `tls` and `wss`, case-insensitively.
260    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/// Frame timestamp, either time-only or full date+time.
276///
277/// Older FreeSWITCH versions write `HH:MM:SS.usec`, newer versions write
278/// `YYYY-MM-DD HH:MM:SS.usec`. Both formats are supported.
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum Timestamp {
281    /// `HH:MM:SS.usec` — no date component.
282    TimeOnly {
283        /// Hour (0-23).
284        hour: u8,
285        /// Minute (0-59).
286        min: u8,
287        /// Second (0-59).
288        sec: u8,
289        /// Microseconds (0-999999).
290        usec: u32,
291    },
292    /// `YYYY-MM-DD HH:MM:SS.usec` — full date and time.
293    DateTime {
294        /// Year.
295        year: u16,
296        /// Month (1-12).
297        month: u8,
298        /// Day (1-31).
299        day: u8,
300        /// Hour (0-23).
301        hour: u8,
302        /// Minute (0-59).
303        min: u8,
304        /// Second (0-59).
305        sec: u8,
306        /// Microseconds (0-999999).
307        usec: u32,
308    },
309}
310
311impl Timestamp {
312    /// Seconds since midnight, ignoring microseconds.
313    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    /// Tuple suitable for chronological ordering.
322    /// `TimeOnly` timestamps sort before any `DateTime` (year/month/day = 0).
323    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
344/// Howard Hinnant's `days_from_civil` — proleptic Gregorian days since
345/// 1970-01-01. Pure integer math, valid for the entire i64 range.
346pub(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/// Elapsed time over a dump stream, for parsers that drop state a connection
357/// or dialog has stopped feeding.
358///
359/// A dated timestamp gives absolute seconds from its own date. A time-only
360/// timestamp has no date, so it gets a synthetic day counter that increments
361/// when the clock wraps past midnight. The two carry no common epoch: a stream
362/// that changes format resets the clock, and the sweep that would follow that
363/// reset is skipped, since every recorded time then belongs to the other
364/// domain.
365#[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    /// How long a connection or dialog may stay silent before its pending
377    /// state is dropped: the RFC 793 default TCP keepalive timeout, beyond
378    /// which a VoIP connection is dead.
379    pub const TIMEOUT_SECS: u64 = 7200;
380
381    /// A clock that has seen no timestamp yet.
382    pub fn new() -> Self {
383        Self::default()
384    }
385
386    /// Record a timestamp and return the stream time it reads, in seconds.
387    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    /// The stream time of the last observed timestamp, in seconds.
416    pub fn now(&self) -> u64 {
417        self.now
418    }
419
420    /// Whether a stale sweep is due, marking one as taken when it is. The
421    /// first sweep after a format change is not due: no recorded time is
422    /// comparable to the clock's new domain.
423    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    /// Whether a time recorded from [`now`](Self::now) has gone stale.
437    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/// The transport metadata every level carries: who the peer was, over what,
468/// in which direction, and when.
469///
470/// Borrows the address from the frame or message it describes, so it costs no
471/// allocation and cannot drift from its source.
472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473pub struct FrameMeta<'a> {
474    /// Whether the frame was received or sent.
475    pub direction: Direction,
476    /// Transport protocol.
477    pub transport: Transport,
478    /// Remote address as recorded in the frame header.
479    pub address: &'a str,
480    /// When the frame was logged.
481    pub timestamp: Timestamp,
482}
483
484impl FrameMeta<'_> {
485    /// The remote address as a typed [`SocketAddr`], preserving family and
486    /// port. `None` when the recorded address is not `ip:port`; the raw string
487    /// remains in [`address`](Self::address).
488    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/// A single frame from the dump file (Level 1 output).
508///
509/// Each frame corresponds to one `send()` or `recv()` call logged by
510/// `mod_sofia`. The `byte_count` field is the value FreeSWITCH wrote in the
511/// header; `content` is the actual payload between boundaries.
512#[derive(Debug, Clone)]
513pub struct Frame {
514    /// Whether this frame was received or sent.
515    pub direction: Direction,
516    /// Byte count declared in the frame header.
517    pub byte_count: usize,
518    /// Transport protocol.
519    pub transport: Transport,
520    /// Remote address as `ip:port` (e.g., `"10.0.0.1:5060"`).
521    pub address: String,
522    /// When this frame was logged.
523    pub timestamp: Timestamp,
524    /// Raw frame payload.
525    pub content: Vec<u8>,
526}
527
528impl Frame {
529    /// Direction, transport, address and timestamp as one borrowed value.
530    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    /// The remote address as a typed [`SocketAddr`], preserving family and
540    /// port. `None` when the recorded address is not `ip:port`; the raw string
541    /// remains in [`address`](Self::address).
542    pub fn socket_addr(&self) -> Option<SocketAddr> {
543        self.meta().socket_addr()
544    }
545}
546
547/// A reassembled SIP message (Level 2 output).
548///
549/// For TCP, consecutive frames from the same connection are concatenated and
550/// split by Content-Length. For UDP, each frame becomes one message (1:1).
551#[derive(Debug, Clone)]
552pub struct SipMessage {
553    /// Whether this message was received or sent.
554    pub direction: Direction,
555    /// Transport protocol.
556    pub transport: Transport,
557    /// Remote address as `ip:port`.
558    pub address: String,
559    /// Timestamp of the first frame in this message.
560    pub timestamp: Timestamp,
561    /// Reassembled message bytes (headers + body).
562    pub content: Vec<u8>,
563    /// Number of Level 1 frames that were reassembled into this message.
564    pub frame_count: usize,
565}
566
567impl SipMessage {
568    /// Direction, transport, address and timestamp as one borrowed value.
569    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    /// The remote address as a typed [`SocketAddr`], preserving family and
579    /// port. `None` when the recorded address is not `ip:port`; the raw string
580    /// remains in [`address`](Self::address).
581    pub fn socket_addr(&self) -> Option<SocketAddr> {
582        self.meta().socket_addr()
583    }
584}
585
586/// SIP request or response first line.
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub enum SipMessageType {
589    /// `METHOD uri SIP/2.0`
590    Request {
591        /// SIP method (e.g., `"INVITE"`, `"BYE"`).
592        method: String,
593        /// Request URI.
594        uri: String,
595    },
596    /// `SIP/2.0 code reason`
597    Response {
598        /// Status code (e.g., 200, 404).
599        code: u16,
600        /// Reason phrase (e.g., `"OK"`, `"Not Found"`).
601        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    /// Short description: the method name for requests, `"code reason"` for responses.
616    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/// Headers in wire order as `(name, value)` pairs. Names preserve original
625/// casing; [`value`](Self::value) is the case-insensitive lookup.
626#[derive(Debug, Clone, Default, PartialEq, Eq)]
627pub struct Headers(Vec<(String, String)>);
628
629impl Headers {
630    /// Every value recorded under `name`, case-insensitively, in wire order.
631    /// Compact forms are not resolved, as for [`value`](Self::value).
632    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    /// Case-insensitive header lookup, first match in wire order. Compact
640    /// forms are not resolved: ask for the name the message is expected to
641    /// carry.
642    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/// A fully parsed SIP message (Level 3 output).
686///
687/// Provides typed access to the request/response line, headers, and body.
688/// For JSON content types, [`body_text()`](Self::body_text) unescapes RFC 8259
689/// string sequences. For multipart bodies, [`body_parts()`](Self::body_parts)
690/// splits into individual MIME parts.
691#[derive(Debug, Clone)]
692pub struct ParsedSipMessage {
693    /// Whether this message was received or sent.
694    pub direction: Direction,
695    /// Transport protocol.
696    pub transport: Transport,
697    /// Remote address as `ip:port`.
698    pub address: String,
699    /// When this message was logged.
700    pub timestamp: Timestamp,
701    /// Parsed request or response first line.
702    pub message_type: SipMessageType,
703    /// Message headers in wire order.
704    pub headers: Headers,
705    /// Raw body bytes after the `\r\n\r\n` header terminator.
706    pub body: Vec<u8>,
707    /// Number of Level 1 frames that were reassembled into this message.
708    pub frame_count: usize,
709}
710
711/// A `message/sipfrag` body (RFC 3420): any prefix of a SIP message.
712///
713/// Unlike [`ParsedSipMessage`], every element is optional — a fragment may
714/// carry a start line, headers, a body, or any combination, and needs no
715/// trailing CRLF. It has no transport metadata of its own; that belongs to
716/// the message carrying it.
717#[derive(Debug, Clone, PartialEq, Eq)]
718pub struct SipFragment {
719    /// Request or status line, when the fragment begins with one.
720    pub message_type: Option<SipMessageType>,
721    /// Fragment headers in wire order.
722    pub headers: Headers,
723    /// Body bytes after the `\r\n\r\n` terminator, empty when absent.
724    pub body: Vec<u8>,
725}
726
727impl SipFragment {
728    /// Case-insensitive header lookup, first match in wire order.
729    pub fn header_value(&self, name: &str) -> Option<&str> {
730        self.headers.value(name)
731    }
732
733    /// Returns the Content-Type header value. Checks both `Content-Type` and
734    /// the compact form `c`.
735    pub fn content_type(&self) -> Option<&str> {
736        value_or_compact(&self.headers, "Content-Type")
737    }
738}
739
740/// A single part from a multipart MIME body.
741#[derive(Debug, Clone, PartialEq, Eq)]
742pub struct MimePart {
743    /// Part headers in wire order (e.g., Content-Type, Content-ID).
744    pub headers: Headers,
745    /// Part body bytes.
746    pub body: Vec<u8>,
747}
748
749impl MimePart {
750    /// Returns the Content-Type header value. Checks both `Content-Type` and
751    /// the compact form `c`.
752    pub fn content_type(&self) -> Option<&str> {
753        value_or_compact(&self.headers, "Content-Type")
754    }
755
756    /// Case-insensitive header lookup, first match in wire order.
757    pub fn header_value(&self, name: &str) -> Option<&str> {
758        self.headers.value(name)
759    }
760
761    /// Returns the Content-ID header value, if present.
762    pub fn content_id(&self) -> Option<&str> {
763        self.header_value("Content-ID")
764    }
765
766    /// Returns the Content-Disposition header value, if present.
767    pub fn content_disposition(&self) -> Option<&str> {
768        self.header_value("Content-Disposition")
769    }
770
771    /// Returns the Content-Transfer-Encoding header value, if present. A value
772    /// the caller does not recognize means the part's bytes are not what its
773    /// media type describes.
774    pub fn content_transfer_encoding(&self) -> Option<&str> {
775        self.header_value("Content-Transfer-Encoding")
776    }
777}
778
779impl ParsedSipMessage {
780    /// Direction, transport, address and timestamp as one borrowed value.
781    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    /// The remote address as a typed [`SocketAddr`], preserving family and
791    /// port. `None` when the recorded address is not `ip:port`; the raw string
792    /// remains in [`address`](Self::address).
793    pub fn socket_addr(&self) -> Option<SocketAddr> {
794        self.meta().socket_addr()
795    }
796
797    /// Returns the Call-ID header value. Checks both `Call-ID` and
798    /// the compact form `i`.
799    pub fn call_id(&self) -> Option<&str> {
800        value_or_compact(&self.headers, "Call-ID")
801    }
802
803    /// Returns the Content-Type header value. Checks both `Content-Type` and
804    /// the compact form `c`.
805    pub fn content_type(&self) -> Option<&str> {
806        value_or_compact(&self.headers, "Content-Type")
807    }
808
809    /// Returns the Content-Length header value as `usize`. Checks both
810    /// `Content-Length` and the compact form `l`.
811    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    /// Returns the CSeq header value (e.g., `"1 INVITE"`).
816    pub fn cseq(&self) -> Option<&str> {
817        self.header_value("CSeq")
818    }
819
820    /// Returns the SIP method: from the request line for requests,
821    /// or from the CSeq header for responses.
822    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    /// Raw body bytes interpreted as UTF-8 (lossy). No processing is applied
832    /// regardless of Content-Type.
833    pub fn body_data(&self) -> Cow<'_, str> {
834        String::from_utf8_lossy(&self.body)
835    }
836
837    /// Reconstruct the SIP message as wire-format bytes (first line + headers + body).
838    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    /// Case-insensitive header lookup, first match in wire order. Compact
857    /// forms are not resolved; the typed accessors above check both names.
858    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}