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)]
47pub enum SkipReason {
48    /// Truncated frame at the start of a file, typically from logrotate
49    /// cutting mid-write. Capped at 65,537 bytes: the largest datagram plus
50    /// the two-byte boundary.
51    PartialFirstFrame,
52    /// Skip region exceeds 65,537 bytes, at file start or mid-stream,
53    /// indicating the input is not a dump file (e.g., compressed or binary
54    /// data).
55    OversizedFrame,
56    /// Unrecoverable bytes skipped between valid frames mid-stream.
57    MidStreamSkip,
58    /// Logrotate wrote a partial frame tail at the start of the new file.
59    /// Detected by the `\r\n\r\n\x0B\n` suffix pattern.
60    ReplayedFrame,
61    /// Frame at EOF with fewer content bytes than declared in the header.
62    IncompleteFrame,
63    /// Data starts with `recv`/`sent` but fails frame header parsing.
64    InvalidHeader,
65}
66
67impl fmt::Display for SkipReason {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            SkipReason::PartialFirstFrame => f.write_str("partial first frame"),
71            SkipReason::OversizedFrame => f.write_str("oversized frame"),
72            SkipReason::MidStreamSkip => f.write_str("mid-stream skip"),
73            SkipReason::ReplayedFrame => f.write_str("replayed frame (logrotate)"),
74            SkipReason::IncompleteFrame => f.write_str("incomplete frame"),
75            SkipReason::InvalidHeader => f.write_str("invalid header"),
76        }
77    }
78}
79
80/// Controls how much detail the parser records about unparsed regions.
81///
82/// Defaults to `CountOnly` for constant-memory operation. Higher levels
83/// allocate per-region and should only be enabled for diagnostics.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SkipTracking {
86    /// Track only `bytes_read` and `bytes_skipped` counters. No allocation.
87    CountOnly,
88    /// Record offset, length, and reason for each unparsed region.
89    TrackRegions,
90    /// Like `TrackRegions`, but also capture the skipped bytes themselves.
91    CaptureData,
92}
93
94/// A contiguous region of the input that was not parsed into a frame.
95#[derive(Debug, Clone)]
96pub struct UnparsedRegion {
97    /// Byte offset from the start of the input stream.
98    pub offset: u64,
99    /// Number of bytes in this region.
100    pub length: u64,
101    /// Why this region was skipped.
102    pub reason: SkipReason,
103    /// The raw bytes, populated only when [`SkipTracking::CaptureData`] is enabled.
104    pub data: Option<Vec<u8>>,
105}
106
107/// Byte-level parse coverage statistics.
108///
109/// Available from all three iterator levels via `stats()` or `parse_stats()`.
110/// Every byte consumed from the reader is accounted for as either parsed
111/// (`bytes_read - bytes_skipped`) or skipped (`bytes_skipped`).
112#[derive(Debug, Default, Clone)]
113pub struct ParseStats {
114    /// Total bytes consumed from the reader.
115    pub bytes_read: u64,
116    /// Bytes that were skipped (not parsed into frames).
117    pub bytes_skipped: u64,
118    /// Detailed unparsed region records. Only populated when
119    /// [`SkipTracking`] is `TrackRegions` or `CaptureData`.
120    pub unparsed_regions: Vec<UnparsedRegion>,
121}
122
123impl ParseStats {
124    /// Take all accumulated unparsed regions, leaving the list empty.
125    pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
126        std::mem::take(&mut self.unparsed_regions)
127    }
128}
129
130/// The input named none of the keywords a [`Direction`] or [`Transport`]
131/// is written as in a frame header.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct UnknownKeyword(String);
134
135impl UnknownKeyword {
136    /// The rejected input.
137    pub fn as_str(&self) -> &str {
138        &self.0
139    }
140}
141
142impl fmt::Display for UnknownKeyword {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "unknown keyword: {}", self.0)
145    }
146}
147
148impl std::error::Error for UnknownKeyword {}
149
150/// Whether a frame was received or sent by FreeSWITCH.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum Direction {
153    /// Received from the network.
154    Recv,
155    /// Sent to the network.
156    Sent,
157}
158
159impl fmt::Display for Direction {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.write_str(self.as_str())
162    }
163}
164
165impl std::str::FromStr for Direction {
166    type Err = UnknownKeyword;
167
168    /// Accepts `recv` and `sent`, case-insensitively.
169    fn from_str(s: &str) -> Result<Self, Self::Err> {
170        for candidate in [Direction::Recv, Direction::Sent] {
171            if s.eq_ignore_ascii_case(candidate.as_str()) {
172                return Ok(candidate);
173            }
174        }
175        Err(UnknownKeyword(s.to_string()))
176    }
177}
178
179impl Direction {
180    /// The keyword a frame header spells this direction with.
181    pub fn as_str(&self) -> &'static str {
182        match self {
183            Direction::Recv => "recv",
184            Direction::Sent => "sent",
185        }
186    }
187
188    /// Returns `"from"` for `Recv`, `"to"` for `Sent`.
189    pub fn preposition(&self) -> &'static str {
190        match self {
191            Direction::Recv => "from",
192            Direction::Sent => "to",
193        }
194    }
195}
196
197/// SIP transport protocol as reported in the frame header.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
199pub enum Transport {
200    /// Transmission Control Protocol.
201    Tcp,
202    /// User Datagram Protocol.
203    Udp,
204    /// Transport Layer Security.
205    Tls,
206    /// WebSocket Secure (RFC 7118).
207    Wss,
208}
209
210impl Transport {
211    /// The keyword a frame header spells this transport with.
212    pub fn as_str(&self) -> &'static str {
213        match self {
214            Transport::Tcp => "tcp",
215            Transport::Udp => "udp",
216            Transport::Tls => "tls",
217            Transport::Wss => "wss",
218        }
219    }
220}
221
222impl fmt::Display for Transport {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.write_str(self.as_str())
225    }
226}
227
228impl std::str::FromStr for Transport {
229    type Err = UnknownKeyword;
230
231    /// Accepts `tcp`, `udp`, `tls` and `wss`, case-insensitively.
232    fn from_str(s: &str) -> Result<Self, Self::Err> {
233        for candidate in [
234            Transport::Tcp,
235            Transport::Udp,
236            Transport::Tls,
237            Transport::Wss,
238        ] {
239            if s.eq_ignore_ascii_case(candidate.as_str()) {
240                return Ok(candidate);
241            }
242        }
243        Err(UnknownKeyword(s.to_string()))
244    }
245}
246
247/// Frame timestamp, either time-only or full date+time.
248///
249/// Older FreeSWITCH versions write `HH:MM:SS.usec`, newer versions write
250/// `YYYY-MM-DD HH:MM:SS.usec`. Both formats are supported.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum Timestamp {
253    /// `HH:MM:SS.usec` — no date component.
254    TimeOnly {
255        /// Hour (0-23).
256        hour: u8,
257        /// Minute (0-59).
258        min: u8,
259        /// Second (0-59).
260        sec: u8,
261        /// Microseconds (0-999999).
262        usec: u32,
263    },
264    /// `YYYY-MM-DD HH:MM:SS.usec` — full date and time.
265    DateTime {
266        /// Year.
267        year: u16,
268        /// Month (1-12).
269        month: u8,
270        /// Day (1-31).
271        day: u8,
272        /// Hour (0-23).
273        hour: u8,
274        /// Minute (0-59).
275        min: u8,
276        /// Second (0-59).
277        sec: u8,
278        /// Microseconds (0-999999).
279        usec: u32,
280    },
281}
282
283impl Timestamp {
284    /// Seconds since midnight, ignoring microseconds.
285    pub fn time_of_day_secs(&self) -> u32 {
286        let (h, m, s) = match self {
287            Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
288            Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
289        };
290        h as u32 * 3600 + m as u32 * 60 + s as u32
291    }
292
293    /// Tuple suitable for chronological ordering.
294    /// `TimeOnly` timestamps sort before any `DateTime` (year/month/day = 0).
295    pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
296        match self {
297            Timestamp::TimeOnly {
298                hour,
299                min,
300                sec,
301                usec,
302            } => (0, 0, 0, *hour, *min, *sec, *usec),
303            Timestamp::DateTime {
304                year,
305                month,
306                day,
307                hour,
308                min,
309                sec,
310                usec,
311            } => (*year, *month, *day, *hour, *min, *sec, *usec),
312        }
313    }
314}
315
316/// Howard Hinnant's `days_from_civil` — proleptic Gregorian days since
317/// 1970-01-01. Pure integer math, valid for the entire i64 range.
318pub(crate) fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
319    let y = if m <= 2 { y - 1 } else { y };
320    let era = if y >= 0 { y } else { y - 399 } / 400;
321    let yoe = (y - era * 400) as u64;
322    let m_adj = if m > 2 { m as i64 - 3 } else { m as i64 + 9 } as u64;
323    let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
324    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
325    era * 146097 + doe as i64 - 719468
326}
327
328/// Elapsed time over a dump stream, for parsers that drop state a connection
329/// or dialog has stopped feeding.
330///
331/// A dated timestamp gives absolute seconds from its own date. A time-only
332/// timestamp has no date, so it gets a synthetic day counter that increments
333/// when the clock wraps past midnight. The two carry no common epoch: a stream
334/// that changes format resets the clock, and the sweep that would follow that
335/// reset is skipped, since every recorded time then belongs to the other
336/// domain.
337#[derive(Debug, Default, Clone)]
338pub struct StaleClock {
339    day: u32,
340    last_time_secs: u32,
341    now: u64,
342    last_sweep: u64,
343    dated: Option<bool>,
344    reset: bool,
345}
346
347impl StaleClock {
348    /// How long a connection or dialog may stay silent before its pending
349    /// state is dropped: the RFC 793 default TCP keepalive timeout, beyond
350    /// which a VoIP connection is dead.
351    pub const TIMEOUT_SECS: u64 = 7200;
352
353    /// A clock that has seen no timestamp yet.
354    pub fn new() -> Self {
355        Self::default()
356    }
357
358    /// Record a timestamp and return the stream time it reads, in seconds.
359    pub fn observe(&mut self, timestamp: Timestamp) -> u64 {
360        let dated = matches!(timestamp, Timestamp::DateTime { .. });
361        if self.dated != Some(dated) {
362            self.dated = Some(dated);
363            self.day = 0;
364            self.last_time_secs = 0;
365            self.reset = true;
366        }
367
368        let time_secs = timestamp.time_of_day_secs();
369        self.now = match timestamp {
370            Timestamp::DateTime {
371                year, month, day, ..
372            } => {
373                let days = days_from_civil(year as i64, month as u32, day as u32).max(0) as u64;
374                days * 86400 + time_secs as u64
375            }
376            Timestamp::TimeOnly { .. } => {
377                if time_secs < self.last_time_secs && self.last_time_secs - time_secs > 43200 {
378                    self.day += 1;
379                }
380                self.day as u64 * 86400 + time_secs as u64
381            }
382        };
383        self.last_time_secs = time_secs;
384        self.now
385    }
386
387    /// The stream time of the last observed timestamp, in seconds.
388    pub fn now(&self) -> u64 {
389        self.now
390    }
391
392    /// Whether a stale sweep is due, marking one as taken when it is. The
393    /// first sweep after a format change is not due: no recorded time is
394    /// comparable to the clock's new domain.
395    pub fn sweep_due(&mut self) -> bool {
396        if self.reset {
397            self.reset = false;
398            self.last_sweep = self.now;
399            return false;
400        }
401        if self.now.saturating_sub(self.last_sweep) >= Self::TIMEOUT_SECS {
402            self.last_sweep = self.now;
403            return true;
404        }
405        false
406    }
407
408    /// Whether a time recorded from [`now`](Self::now) has gone stale.
409    pub fn is_stale(&self, last_seen: u64) -> bool {
410        self.now.saturating_sub(last_seen) > Self::TIMEOUT_SECS
411    }
412}
413
414impl fmt::Display for Timestamp {
415    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416        match self {
417            Timestamp::TimeOnly {
418                hour,
419                min,
420                sec,
421                usec,
422            } => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
423            Timestamp::DateTime {
424                year,
425                month,
426                day,
427                hour,
428                min,
429                sec,
430                usec,
431            } => write!(
432                f,
433                "{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
434            ),
435        }
436    }
437}
438
439/// The transport metadata every level carries: who the peer was, over what,
440/// in which direction, and when.
441///
442/// Borrows the address from the frame or message it describes, so it costs no
443/// allocation and cannot drift from its source.
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub struct FrameMeta<'a> {
446    /// Whether the frame was received or sent.
447    pub direction: Direction,
448    /// Transport protocol.
449    pub transport: Transport,
450    /// Remote address as recorded in the frame header.
451    pub address: &'a str,
452    /// When the frame was logged.
453    pub timestamp: Timestamp,
454}
455
456impl FrameMeta<'_> {
457    /// The remote address as a typed [`SocketAddr`], preserving family and
458    /// port. `None` when the recorded address is not `ip:port`; the raw string
459    /// remains in [`address`](Self::address).
460    pub fn socket_addr(&self) -> Option<SocketAddr> {
461        parse_socket_addr(self.address)
462    }
463}
464
465impl fmt::Display for FrameMeta<'_> {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        write!(
468            f,
469            "{} {} {}/{} at {}",
470            self.direction,
471            self.direction.preposition(),
472            self.transport,
473            self.address,
474            self.timestamp
475        )
476    }
477}
478
479/// A single frame from the dump file (Level 1 output).
480///
481/// Each frame corresponds to one `send()` or `recv()` call logged by
482/// `mod_sofia`. The `byte_count` field is the value FreeSWITCH wrote in the
483/// header; `content` is the actual payload between boundaries.
484#[derive(Debug, Clone)]
485pub struct Frame {
486    /// Whether this frame was received or sent.
487    pub direction: Direction,
488    /// Byte count declared in the frame header.
489    pub byte_count: usize,
490    /// Transport protocol.
491    pub transport: Transport,
492    /// Remote address as `ip:port` (e.g., `"10.0.0.1:5060"`).
493    pub address: String,
494    /// When this frame was logged.
495    pub timestamp: Timestamp,
496    /// Raw frame payload.
497    pub content: Vec<u8>,
498}
499
500impl Frame {
501    /// Direction, transport, address and timestamp as one borrowed value.
502    pub fn meta(&self) -> FrameMeta<'_> {
503        FrameMeta {
504            direction: self.direction,
505            transport: self.transport,
506            address: &self.address,
507            timestamp: self.timestamp,
508        }
509    }
510
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        self.meta().socket_addr()
516    }
517}
518
519/// A reassembled SIP message (Level 2 output).
520///
521/// For TCP, consecutive frames from the same connection are concatenated and
522/// split by Content-Length. For UDP, each frame becomes one message (1:1).
523#[derive(Debug, Clone)]
524pub struct SipMessage {
525    /// Whether this message was received or sent.
526    pub direction: Direction,
527    /// Transport protocol.
528    pub transport: Transport,
529    /// Remote address as `ip:port`.
530    pub address: String,
531    /// Timestamp of the first frame in this message.
532    pub timestamp: Timestamp,
533    /// Reassembled message bytes (headers + body).
534    pub content: Vec<u8>,
535    /// Number of Level 1 frames that were reassembled into this message.
536    pub frame_count: usize,
537}
538
539impl SipMessage {
540    /// Direction, transport, address and timestamp as one borrowed value.
541    pub fn meta(&self) -> FrameMeta<'_> {
542        FrameMeta {
543            direction: self.direction,
544            transport: self.transport,
545            address: &self.address,
546            timestamp: self.timestamp,
547        }
548    }
549
550    /// The remote address as a typed [`SocketAddr`], preserving family and
551    /// port. `None` when the recorded address is not `ip:port`; the raw string
552    /// remains in [`address`](Self::address).
553    pub fn socket_addr(&self) -> Option<SocketAddr> {
554        self.meta().socket_addr()
555    }
556}
557
558/// SIP request or response first line.
559#[derive(Debug, Clone, PartialEq, Eq)]
560pub enum SipMessageType {
561    /// `METHOD uri SIP/2.0`
562    Request {
563        /// SIP method (e.g., `"INVITE"`, `"BYE"`).
564        method: String,
565        /// Request URI.
566        uri: String,
567    },
568    /// `SIP/2.0 code reason`
569    Response {
570        /// Status code (e.g., 200, 404).
571        code: u16,
572        /// Reason phrase (e.g., `"OK"`, `"Not Found"`).
573        reason: String,
574    },
575}
576
577impl fmt::Display for SipMessageType {
578    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579        match self {
580            SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
581            SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
582        }
583    }
584}
585
586impl SipMessageType {
587    /// Short description: the method name for requests, `"code reason"` for responses.
588    pub fn summary(&self) -> Cow<'_, str> {
589        match self {
590            SipMessageType::Request { method, .. } => Cow::Borrowed(method),
591            SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
592        }
593    }
594}
595
596/// Headers in wire order as `(name, value)` pairs. Names preserve original
597/// casing; [`value`](Self::value) is the case-insensitive lookup.
598#[derive(Debug, Clone, Default, PartialEq, Eq)]
599pub struct Headers(pub Vec<(String, String)>);
600
601impl Headers {
602    /// Every value recorded under `name`, case-insensitively, in wire order.
603    /// Compact forms are not resolved, as for [`value`](Self::value).
604    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
605        self.0
606            .iter()
607            .filter(move |(k, _)| k.eq_ignore_ascii_case(name))
608            .map(|(_, v)| v.as_str())
609    }
610
611    /// Case-insensitive header lookup, first match in wire order. Compact
612    /// forms are not resolved: ask for the name the message is expected to
613    /// carry.
614    pub fn value(&self, name: &str) -> Option<&str> {
615        self.0
616            .iter()
617            .find(|(k, _)| k.eq_ignore_ascii_case(name))
618            .map(|(_, v)| v.as_str())
619    }
620}
621
622impl std::ops::Deref for Headers {
623    type Target = [(String, String)];
624
625    fn deref(&self) -> &Self::Target {
626        &self.0
627    }
628}
629
630impl std::ops::DerefMut for Headers {
631    fn deref_mut(&mut self) -> &mut Self::Target {
632        &mut self.0
633    }
634}
635
636impl From<Vec<(String, String)>> for Headers {
637    fn from(headers: Vec<(String, String)>) -> Self {
638        Headers(headers)
639    }
640}
641
642impl FromIterator<(String, String)> for Headers {
643    fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
644        Headers(iter.into_iter().collect())
645    }
646}
647
648impl<'a> IntoIterator for &'a Headers {
649    type Item = &'a (String, String);
650    type IntoIter = std::slice::Iter<'a, (String, String)>;
651
652    fn into_iter(self) -> Self::IntoIter {
653        self.0.iter()
654    }
655}
656
657/// A fully parsed SIP message (Level 3 output).
658///
659/// Provides typed access to the request/response line, headers, and body.
660/// For JSON content types, [`body_text()`](Self::body_text) unescapes RFC 8259
661/// string sequences. For multipart bodies, [`body_parts()`](Self::body_parts)
662/// splits into individual MIME parts.
663#[derive(Debug, Clone)]
664pub struct ParsedSipMessage {
665    /// Whether this message was received or sent.
666    pub direction: Direction,
667    /// Transport protocol.
668    pub transport: Transport,
669    /// Remote address as `ip:port`.
670    pub address: String,
671    /// When this message was logged.
672    pub timestamp: Timestamp,
673    /// Parsed request or response first line.
674    pub message_type: SipMessageType,
675    /// Message headers in wire order.
676    pub headers: Headers,
677    /// Raw body bytes after the `\r\n\r\n` header terminator.
678    pub body: Vec<u8>,
679    /// Number of Level 1 frames that were reassembled into this message.
680    pub frame_count: usize,
681}
682
683/// A `message/sipfrag` body (RFC 3420): any prefix of a SIP message.
684///
685/// Unlike [`ParsedSipMessage`], every element is optional — a fragment may
686/// carry a start line, headers, a body, or any combination, and needs no
687/// trailing CRLF. It has no transport metadata of its own; that belongs to
688/// the message carrying it.
689#[derive(Debug, Clone, PartialEq, Eq)]
690pub struct SipFragment {
691    /// Request or status line, when the fragment begins with one.
692    pub message_type: Option<SipMessageType>,
693    /// Fragment headers in wire order.
694    pub headers: Headers,
695    /// Body bytes after the `\r\n\r\n` terminator, empty when absent.
696    pub body: Vec<u8>,
697}
698
699impl SipFragment {
700    /// Case-insensitive header lookup, first match in wire order.
701    pub fn header_value(&self, name: &str) -> Option<&str> {
702        self.headers.value(name)
703    }
704
705    /// Returns the Content-Type header value. Checks both `Content-Type` and
706    /// the compact form `c`.
707    pub fn content_type(&self) -> Option<&str> {
708        value_or_compact(&self.headers, "Content-Type")
709    }
710}
711
712/// A single part from a multipart MIME body.
713#[derive(Debug, Clone, PartialEq, Eq)]
714pub struct MimePart {
715    /// Part headers in wire order (e.g., Content-Type, Content-ID).
716    pub headers: Headers,
717    /// Part body bytes.
718    pub body: Vec<u8>,
719}
720
721impl MimePart {
722    /// Returns the Content-Type header value. Checks both `Content-Type` and
723    /// the compact form `c`.
724    pub fn content_type(&self) -> Option<&str> {
725        value_or_compact(&self.headers, "Content-Type")
726    }
727
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-ID header value, if present.
734    pub fn content_id(&self) -> Option<&str> {
735        self.header_value("Content-ID")
736    }
737
738    /// Returns the Content-Disposition header value, if present.
739    pub fn content_disposition(&self) -> Option<&str> {
740        self.header_value("Content-Disposition")
741    }
742
743    /// Returns the Content-Transfer-Encoding header value, if present. A value
744    /// the caller does not recognize means the part's bytes are not what its
745    /// media type describes.
746    pub fn content_transfer_encoding(&self) -> Option<&str> {
747        self.header_value("Content-Transfer-Encoding")
748    }
749}
750
751impl ParsedSipMessage {
752    /// Direction, transport, address and timestamp as one borrowed value.
753    pub fn meta(&self) -> FrameMeta<'_> {
754        FrameMeta {
755            direction: self.direction,
756            transport: self.transport,
757            address: &self.address,
758            timestamp: self.timestamp,
759        }
760    }
761
762    /// The remote address as a typed [`SocketAddr`], preserving family and
763    /// port. `None` when the recorded address is not `ip:port`; the raw string
764    /// remains in [`address`](Self::address).
765    pub fn socket_addr(&self) -> Option<SocketAddr> {
766        self.meta().socket_addr()
767    }
768
769    /// Returns the Call-ID header value. Checks both `Call-ID` and
770    /// the compact form `i`.
771    pub fn call_id(&self) -> Option<&str> {
772        value_or_compact(&self.headers, "Call-ID")
773    }
774
775    /// Returns the Content-Type header value. Checks both `Content-Type` and
776    /// the compact form `c`.
777    pub fn content_type(&self) -> Option<&str> {
778        value_or_compact(&self.headers, "Content-Type")
779    }
780
781    /// Returns the Content-Length header value as `usize`. Checks both
782    /// `Content-Length` and the compact form `l`.
783    pub fn content_length(&self) -> Option<usize> {
784        value_or_compact(&self.headers, "Content-Length").and_then(|v| v.trim().parse().ok())
785    }
786
787    /// Returns the CSeq header value (e.g., `"1 INVITE"`).
788    pub fn cseq(&self) -> Option<&str> {
789        self.header_value("CSeq")
790    }
791
792    /// Returns the SIP method: from the request line for requests,
793    /// or from the CSeq header for responses.
794    pub fn method(&self) -> Option<&str> {
795        match &self.message_type {
796            SipMessageType::Request { method, .. } => Some(method),
797            SipMessageType::Response { .. } => {
798                self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
799            }
800        }
801    }
802
803    /// Raw body bytes interpreted as UTF-8 (lossy). No processing is applied
804    /// regardless of Content-Type.
805    pub fn body_data(&self) -> Cow<'_, str> {
806        String::from_utf8_lossy(&self.body)
807    }
808
809    /// Reconstruct the SIP message as wire-format bytes (first line + headers + body).
810    pub fn to_bytes(&self) -> Vec<u8> {
811        let mut out = Vec::new();
812        match &self.message_type {
813            SipMessageType::Request { method, uri } => {
814                out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
815            }
816            SipMessageType::Response { code, reason } => {
817                out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
818            }
819        }
820        for (name, value) in &self.headers {
821            out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
822        }
823        out.extend_from_slice(b"\r\n");
824        out.extend_from_slice(&self.body);
825        out
826    }
827
828    /// Case-insensitive header lookup, first match in wire order. Compact
829    /// forms are not resolved; the typed accessors above check both names.
830    pub fn header_value(&self, name: &str) -> Option<&str> {
831        self.headers.value(name)
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    fn make_parsed(
840        msg_type: SipMessageType,
841        headers: Vec<(&str, &str)>,
842        body: &[u8],
843    ) -> ParsedSipMessage {
844        ParsedSipMessage {
845            direction: Direction::Recv,
846            transport: Transport::Tcp,
847            address: "10.0.0.1:5060".into(),
848            timestamp: Timestamp::TimeOnly {
849                hour: 12,
850                min: 0,
851                sec: 0,
852                usec: 0,
853            },
854            message_type: msg_type,
855            headers: Headers(
856                headers
857                    .iter()
858                    .map(|(k, v)| (k.to_string(), v.to_string()))
859                    .collect(),
860            ),
861            body: body.to_vec(),
862            frame_count: 1,
863        }
864    }
865
866    fn make_frame(address: &str) -> Frame {
867        Frame {
868            direction: Direction::Recv,
869            byte_count: 0,
870            transport: Transport::Tcp,
871            address: address.into(),
872            timestamp: Timestamp::TimeOnly {
873                hour: 0,
874                min: 0,
875                sec: 0,
876                usec: 0,
877            },
878            content: Vec::new(),
879        }
880    }
881
882    fn make_message(address: &str) -> SipMessage {
883        SipMessage {
884            direction: Direction::Recv,
885            transport: Transport::Tcp,
886            address: address.into(),
887            timestamp: Timestamp::TimeOnly {
888                hour: 0,
889                min: 0,
890                sec: 0,
891                usec: 0,
892            },
893            content: Vec::new(),
894            frame_count: 1,
895        }
896    }
897
898    fn parsed_with_address(address: &str) -> ParsedSipMessage {
899        let mut msg = make_parsed(
900            SipMessageType::Request {
901                method: "OPTIONS".into(),
902                uri: "sip:host".into(),
903            },
904            vec![],
905            b"",
906        );
907        msg.address = address.into();
908        msg
909    }
910
911    #[test]
912    fn socket_addr_ipv4() {
913        let addr = make_frame("10.0.0.1:5060").socket_addr().unwrap();
914        assert!(addr.is_ipv4());
915        assert_eq!(addr.port(), 5060);
916        assert_eq!(addr.ip().to_string(), "10.0.0.1");
917    }
918
919    #[test]
920    fn socket_addr_ipv6_bracketed() {
921        let addr = make_message("[2001:db8::1]:5061").socket_addr().unwrap();
922        assert!(addr.is_ipv6());
923        assert_eq!(addr.port(), 5061);
924        assert_eq!(addr.ip().to_string(), "2001:db8::1");
925    }
926
927    #[test]
928    fn socket_addr_ipv4_bracketed() {
929        let addr = make_frame("[198.51.100.7]:5060").socket_addr().unwrap();
930        assert!(addr.is_ipv4());
931        assert_eq!(addr.port(), 5060);
932        assert_eq!(addr.ip().to_string(), "198.51.100.7");
933    }
934
935    #[test]
936    fn socket_addr_on_parsed_message() {
937        let addr = parsed_with_address("192.0.2.4:5080").socket_addr().unwrap();
938        assert_eq!(addr.port(), 5080);
939    }
940
941    #[test]
942    fn socket_addr_rejects_non_addresses() {
943        for bad in [
944            "345.678.987.654:5060",
945            "10.0.0.1",
946            "host.example.test:5060",
947            "2001:db8::1:5060",
948            "",
949        ] {
950            assert!(
951                make_frame(bad).socket_addr().is_none(),
952                "should not parse: {bad}"
953            );
954            assert!(make_message(bad).socket_addr().is_none());
955            assert!(parsed_with_address(bad).socket_addr().is_none());
956        }
957    }
958
959    #[test]
960    fn to_bytes_request_no_body() {
961        let msg = make_parsed(
962            SipMessageType::Request {
963                method: "OPTIONS".into(),
964                uri: "sip:host".into(),
965            },
966            vec![("Call-ID", "test")],
967            b"",
968        );
969        let bytes = msg.to_bytes();
970        let text = String::from_utf8(bytes).unwrap();
971        assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
972        assert!(text.contains("Call-ID: test\r\n"));
973        assert!(text.ends_with("\r\n\r\n"));
974    }
975
976    #[test]
977    fn to_bytes_request_with_body() {
978        let body = b"v=0\r\ns=-\r\n";
979        let msg = make_parsed(
980            SipMessageType::Request {
981                method: "INVITE".into(),
982                uri: "sip:host".into(),
983            },
984            vec![("Call-ID", "test")],
985            body,
986        );
987        let bytes = msg.to_bytes();
988        assert!(bytes.ends_with(body));
989    }
990
991    #[test]
992    fn to_bytes_response() {
993        let msg = make_parsed(
994            SipMessageType::Response {
995                code: 200,
996                reason: "OK".into(),
997            },
998            vec![("Call-ID", "resp-test")],
999            b"",
1000        );
1001        let bytes = msg.to_bytes();
1002        let text = String::from_utf8(bytes).unwrap();
1003        assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
1004    }
1005
1006    #[test]
1007    fn body_data_valid_utf8() {
1008        let msg = make_parsed(
1009            SipMessageType::Request {
1010                method: "MESSAGE".into(),
1011                uri: "sip:host".into(),
1012            },
1013            vec![],
1014            b"hello world",
1015        );
1016        assert_eq!(&*msg.body_data(), "hello world");
1017    }
1018
1019    #[test]
1020    fn body_data_empty() {
1021        let msg = make_parsed(
1022            SipMessageType::Request {
1023                method: "OPTIONS".into(),
1024                uri: "sip:host".into(),
1025            },
1026            vec![],
1027            b"",
1028        );
1029        assert_eq!(&*msg.body_data(), "");
1030    }
1031
1032    #[test]
1033    fn body_data_binary() {
1034        let msg = make_parsed(
1035            SipMessageType::Request {
1036                method: "MESSAGE".into(),
1037                uri: "sip:host".into(),
1038            },
1039            vec![],
1040            &[0xFF, 0xFE],
1041        );
1042        assert!(msg.body_data().contains('\u{FFFD}'));
1043    }
1044
1045    #[test]
1046    fn body_data_preserves_json_escapes() {
1047        let raw = br#"{"key":"value\nwith\\escapes"}"#;
1048        let msg = make_parsed(
1049            SipMessageType::Request {
1050                method: "NOTIFY".into(),
1051                uri: "sip:host".into(),
1052            },
1053            vec![("Content-Type", "application/json")],
1054            raw,
1055        );
1056        assert_eq!(
1057            msg.body_data().as_ref(),
1058            r#"{"key":"value\nwith\\escapes"}"#,
1059            "body_data() must preserve raw escapes"
1060        );
1061    }
1062}