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) 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    /// Total bytes consumed from the reader.
129    pub fn bytes_read(&self) -> u64 {
130        self.bytes_read
131    }
132
133    /// Bytes that were skipped (not parsed into frames).
134    pub fn bytes_skipped(&self) -> u64 {
135        self.bytes_skipped
136    }
137
138    /// Frames whose content ran out before `byte_count` at end of input.
139    /// Maintained in every [`SkipTracking`] mode.
140    pub fn incomplete_frames(&self) -> u64 {
141        self.incomplete_frames
142    }
143
144    /// Total shortfall of those frames. A caller stitching rotated files
145    /// together decides for itself whether a shortfall is a rotation cut.
146    pub fn incomplete_frame_bytes(&self) -> u64 {
147        self.incomplete_frame_bytes
148    }
149
150    /// Connection buffers dropped mid-stream while still holding a partial
151    /// message. Unlike a frame cut at end of input, this content is never
152    /// emitted, so it is lost from the output.
153    pub fn stale_evictions(&self) -> u64 {
154        self.stale_evictions
155    }
156
157    /// Bytes those buffers were holding.
158    pub fn stale_evicted_bytes(&self) -> u64 {
159        self.stale_evicted_bytes
160    }
161
162    /// Times a TCP buffer resynchronised past bytes that began no SIP message.
163    pub fn non_sip_prefixes(&self) -> u64 {
164        self.non_sip_prefixes
165    }
166
167    /// Bytes discarded by those resynchronisations.
168    pub fn non_sip_prefix_bytes(&self) -> u64 {
169        self.non_sip_prefix_bytes
170    }
171
172    /// Detailed unparsed region records. Only populated when
173    /// [`SkipTracking`] is `TrackRegions` or `CaptureData`.
174    pub fn unparsed_regions(&self) -> &[UnparsedRegion] {
175        &self.unparsed_regions
176    }
177
178    /// Take all accumulated unparsed regions, leaving the list empty.
179    pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
180        std::mem::take(&mut self.unparsed_regions)
181    }
182}
183
184/// The input named none of the keywords a [`Direction`] or [`Transport`]
185/// is written as in a frame header.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct UnknownKeyword(String);
188
189impl UnknownKeyword {
190    /// The rejected input.
191    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/// Whether a frame was received or sent by FreeSWITCH.
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206pub enum Direction {
207    /// Received from the network.
208    Recv,
209    /// Sent to the network.
210    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    /// Accepts `recv` and `sent`, case-insensitively.
223    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    /// The keyword a frame header spells this direction with.
235    pub fn as_str(&self) -> &'static str {
236        match self {
237            Direction::Recv => "recv",
238            Direction::Sent => "sent",
239        }
240    }
241
242    /// Returns `"from"` for `Recv`, `"to"` for `Sent`.
243    pub fn preposition(&self) -> &'static str {
244        match self {
245            Direction::Recv => "from",
246            Direction::Sent => "to",
247        }
248    }
249}
250
251/// SIP transport protocol as reported in the frame header.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
253pub enum Transport {
254    /// Transmission Control Protocol.
255    Tcp,
256    /// User Datagram Protocol.
257    Udp,
258    /// Transport Layer Security.
259    Tls,
260    /// WebSocket Secure (RFC 7118).
261    Wss,
262}
263
264impl Transport {
265    /// The keyword a frame header spells this transport with.
266    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    /// Accepts `tcp`, `udp`, `tls` and `wss`, case-insensitively.
286    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/// Frame timestamp, either time-only or full date+time.
302///
303/// Older FreeSWITCH versions write `HH:MM:SS.usec`, newer versions write
304/// `YYYY-MM-DD HH:MM:SS.usec`. Both formats are supported.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum Timestamp {
307    /// `HH:MM:SS.usec` — no date component.
308    TimeOnly {
309        /// Hour (0-23).
310        hour: u8,
311        /// Minute (0-59).
312        min: u8,
313        /// Second (0-59).
314        sec: u8,
315        /// Microseconds (0-999999).
316        usec: u32,
317    },
318    /// `YYYY-MM-DD HH:MM:SS.usec` — full date and time.
319    DateTime {
320        /// Year.
321        year: u16,
322        /// Month (1-12).
323        month: u8,
324        /// Day (1-31).
325        day: u8,
326        /// Hour (0-23).
327        hour: u8,
328        /// Minute (0-59).
329        min: u8,
330        /// Second (0-59).
331        sec: u8,
332        /// Microseconds (0-999999).
333        usec: u32,
334    },
335}
336
337impl Timestamp {
338    /// Seconds since midnight, ignoring microseconds.
339    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    /// Tuple suitable for chronological ordering.
348    /// `TimeOnly` timestamps sort before any `DateTime` (year/month/day = 0).
349    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
370/// Howard Hinnant's `days_from_civil` — proleptic Gregorian days since
371/// 1970-01-01. Pure integer math, valid for the entire i64 range.
372pub(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/// Elapsed time over a dump stream, for parsers that drop state a connection
383/// or dialog has stopped feeding.
384///
385/// A dated timestamp gives absolute seconds from its own date. A time-only
386/// timestamp has no date, so it gets a synthetic day counter that increments
387/// when the clock wraps past midnight. The two carry no common epoch: a stream
388/// that changes format resets the clock, and the sweep that would follow that
389/// reset is skipped, since every recorded time then belongs to the other
390/// domain.
391#[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    /// How long a connection or dialog may stay silent before its pending
403    /// state is dropped: the RFC 793 default TCP keepalive timeout, beyond
404    /// which a VoIP connection is dead.
405    pub const TIMEOUT_SECS: u64 = 7200;
406
407    /// A clock that has seen no timestamp yet.
408    pub fn new() -> Self {
409        Self::default()
410    }
411
412    /// Record a timestamp and return the stream time it reads, in seconds.
413    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    /// The stream time of the last observed timestamp, in seconds.
442    pub fn now(&self) -> u64 {
443        self.now
444    }
445
446    /// Whether a stale sweep is due, marking one as taken when it is. The
447    /// first sweep after a format change is not due: no recorded time is
448    /// comparable to the clock's new domain.
449    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    /// Whether a time recorded from [`now`](Self::now) has gone stale.
463    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/// The transport metadata every level carries: who the peer was, over what,
494/// in which direction, and when.
495///
496/// Borrows the address from the frame or message it describes, so it costs no
497/// allocation and cannot drift from its source.
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub struct FrameMeta<'a> {
500    /// Whether the frame was received or sent.
501    pub direction: Direction,
502    /// Transport protocol.
503    pub transport: Transport,
504    /// Remote address as recorded in the frame header.
505    pub address: &'a str,
506    /// When the frame was logged.
507    pub timestamp: Timestamp,
508}
509
510impl FrameMeta<'_> {
511    /// The remote address as a typed [`SocketAddr`], preserving family and
512    /// port. `None` when the recorded address is not `ip:port`; the raw string
513    /// remains in [`address`](Self::address).
514    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/// A single frame from the dump file (Level 1 output).
534///
535/// Each frame corresponds to one `send()` or `recv()` call logged by
536/// `mod_sofia`. The `byte_count` field is the value FreeSWITCH wrote in the
537/// header; `content` is the actual payload between boundaries.
538#[derive(Debug, Clone)]
539pub struct Frame {
540    /// Whether this frame was received or sent.
541    pub direction: Direction,
542    /// Byte count declared in the frame header.
543    pub byte_count: usize,
544    /// Transport protocol.
545    pub transport: Transport,
546    /// Remote address as `ip:port` (e.g., `"10.0.0.1:5060"`).
547    pub address: String,
548    /// When this frame was logged.
549    pub timestamp: Timestamp,
550    /// Raw frame payload.
551    pub content: Vec<u8>,
552}
553
554impl Frame {
555    /// Direction, transport, address and timestamp as one borrowed value.
556    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    /// The remote address as a typed [`SocketAddr`], preserving family and
566    /// port. `None` when the recorded address is not `ip:port`; the raw string
567    /// remains in [`address`](Self::address).
568    pub fn socket_addr(&self) -> Option<SocketAddr> {
569        self.meta().socket_addr()
570    }
571}
572
573/// A reassembled SIP message (Level 2 output).
574///
575/// For TCP, consecutive frames from the same connection are concatenated and
576/// split by Content-Length. For UDP, each frame becomes one message (1:1).
577#[derive(Debug, Clone)]
578pub struct SipMessage {
579    /// Whether this message was received or sent.
580    pub direction: Direction,
581    /// Transport protocol.
582    pub transport: Transport,
583    /// Remote address as `ip:port`.
584    pub address: String,
585    /// Timestamp of the first frame in this message.
586    pub timestamp: Timestamp,
587    /// Reassembled message bytes (headers + body).
588    pub content: Vec<u8>,
589    /// Number of Level 1 frames that were reassembled into this message.
590    pub frame_count: usize,
591}
592
593impl SipMessage {
594    /// Direction, transport, address and timestamp as one borrowed value.
595    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    /// The remote address as a typed [`SocketAddr`], preserving family and
605    /// port. `None` when the recorded address is not `ip:port`; the raw string
606    /// remains in [`address`](Self::address).
607    pub fn socket_addr(&self) -> Option<SocketAddr> {
608        self.meta().socket_addr()
609    }
610}
611
612/// SIP request or response first line.
613#[derive(Debug, Clone, PartialEq, Eq)]
614pub enum SipMessageType {
615    /// `METHOD uri SIP/2.0`
616    Request {
617        /// SIP method (e.g., `"INVITE"`, `"BYE"`).
618        method: String,
619        /// Request URI.
620        uri: String,
621    },
622    /// `SIP/2.0 code reason`
623    Response {
624        /// Status code (e.g., 200, 404).
625        code: u16,
626        /// Reason phrase (e.g., `"OK"`, `"Not Found"`).
627        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    /// Short description: the method name for requests, `"code reason"` for responses.
642    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/// Headers in wire order as `(name, value)` pairs. Names preserve original
651/// casing; [`value`](Self::value) is the case-insensitive lookup.
652#[derive(Debug, Clone, Default, PartialEq, Eq)]
653pub struct Headers(Vec<(String, String)>);
654
655impl Headers {
656    /// Every value recorded under `name`, case-insensitively, in wire order.
657    /// Compact forms are not resolved, as for [`value`](Self::value).
658    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    /// Case-insensitive header lookup, first match in wire order. Compact
666    /// forms are not resolved: ask for the name the message is expected to
667    /// carry.
668    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/// A fully parsed SIP message (Level 3 output).
712///
713/// Provides typed access to the request/response line, headers, and body.
714/// For JSON content types, [`body_text()`](Self::body_text) unescapes RFC 8259
715/// string sequences. For multipart bodies, [`body_parts()`](Self::body_parts)
716/// splits into individual MIME parts.
717#[derive(Debug, Clone)]
718pub struct ParsedSipMessage {
719    /// Whether this message was received or sent.
720    pub direction: Direction,
721    /// Transport protocol.
722    pub transport: Transport,
723    /// Remote address as `ip:port`.
724    pub address: String,
725    /// When this message was logged.
726    pub timestamp: Timestamp,
727    /// Parsed request or response first line.
728    pub message_type: SipMessageType,
729    /// Message headers in wire order.
730    pub headers: Headers,
731    /// Raw body bytes after the `\r\n\r\n` header terminator.
732    pub body: Vec<u8>,
733    /// Number of Level 1 frames that were reassembled into this message.
734    pub frame_count: usize,
735}
736
737/// A `message/sipfrag` body (RFC 3420): any prefix of a SIP message.
738///
739/// Unlike [`ParsedSipMessage`], every element is optional — a fragment may
740/// carry a start line, headers, a body, or any combination, and needs no
741/// trailing CRLF. It has no transport metadata of its own; that belongs to
742/// the message carrying it.
743#[derive(Debug, Clone, PartialEq, Eq)]
744pub struct SipFragment {
745    /// Request or status line, when the fragment begins with one.
746    pub message_type: Option<SipMessageType>,
747    /// Fragment headers in wire order.
748    pub headers: Headers,
749    /// Body bytes after the `\r\n\r\n` terminator, empty when absent.
750    pub body: Vec<u8>,
751}
752
753impl SipFragment {
754    /// Case-insensitive header lookup, first match in wire order.
755    pub fn header_value(&self, name: &str) -> Option<&str> {
756        self.headers.value(name)
757    }
758
759    /// Returns the Content-Type header value. Checks both `Content-Type` and
760    /// the compact form `c`.
761    pub fn content_type(&self) -> Option<&str> {
762        value_or_compact(&self.headers, "Content-Type")
763    }
764}
765
766/// A single part from a multipart MIME body.
767#[derive(Debug, Clone, PartialEq, Eq)]
768pub struct MimePart {
769    /// Part headers in wire order (e.g., Content-Type, Content-ID).
770    pub headers: Headers,
771    /// Part body bytes.
772    pub body: Vec<u8>,
773}
774
775impl MimePart {
776    /// Returns the Content-Type header value. Checks both `Content-Type` and
777    /// the compact form `c`.
778    pub fn content_type(&self) -> Option<&str> {
779        value_or_compact(&self.headers, "Content-Type")
780    }
781
782    /// Case-insensitive header lookup, first match in wire order.
783    pub fn header_value(&self, name: &str) -> Option<&str> {
784        self.headers.value(name)
785    }
786
787    /// Returns the Content-ID header value, if present.
788    pub fn content_id(&self) -> Option<&str> {
789        self.header_value("Content-ID")
790    }
791
792    /// Returns the Content-Disposition header value, if present.
793    pub fn content_disposition(&self) -> Option<&str> {
794        self.header_value("Content-Disposition")
795    }
796
797    /// Returns the Content-Transfer-Encoding header value, if present. A value
798    /// the caller does not recognize means the part's bytes are not what its
799    /// media type describes.
800    pub fn content_transfer_encoding(&self) -> Option<&str> {
801        self.header_value("Content-Transfer-Encoding")
802    }
803}
804
805impl ParsedSipMessage {
806    /// Direction, transport, address and timestamp as one borrowed value.
807    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    /// The remote address as a typed [`SocketAddr`], preserving family and
817    /// port. `None` when the recorded address is not `ip:port`; the raw string
818    /// remains in [`address`](Self::address).
819    pub fn socket_addr(&self) -> Option<SocketAddr> {
820        self.meta().socket_addr()
821    }
822
823    /// Returns the Call-ID header value. Checks both `Call-ID` and
824    /// the compact form `i`.
825    pub fn call_id(&self) -> Option<&str> {
826        value_or_compact(&self.headers, "Call-ID")
827    }
828
829    /// Returns the Content-Type header value. Checks both `Content-Type` and
830    /// the compact form `c`.
831    pub fn content_type(&self) -> Option<&str> {
832        value_or_compact(&self.headers, "Content-Type")
833    }
834
835    /// Returns the Content-Length header value as `usize`. Checks both
836    /// `Content-Length` and the compact form `l`.
837    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    /// Returns the CSeq header value (e.g., `"1 INVITE"`).
842    pub fn cseq(&self) -> Option<&str> {
843        self.header_value("CSeq")
844    }
845
846    /// Returns the SIP method: from the request line for requests,
847    /// or from the CSeq header for responses.
848    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    /// Raw body bytes interpreted as UTF-8 (lossy). No processing is applied
858    /// regardless of Content-Type.
859    pub fn body_data(&self) -> Cow<'_, str> {
860        String::from_utf8_lossy(&self.body)
861    }
862
863    /// Reconstruct the SIP message as wire-format bytes (first line + headers + body).
864    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    /// Case-insensitive header lookup, first match in wire order. Compact
883    /// forms are not resolved; the typed accessors above check both names.
884    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}