Skip to main content

freeswitch_sofia_trace_parser/sip/
fragment.rs

1use std::borrow::Cow;
2
3use crate::finders::CRLF;
4use crate::frame::ParseError;
5use crate::sip::{parse_headers, split_headers_body, HasHeaders};
6use crate::startline::{is_header_line, parse_first_line};
7use crate::types::{MimePart, SipFragment};
8
9/// Parse a `message/sipfrag` body (RFC 3420) — any prefix of a SIP message.
10///
11/// The start line is optional: a fragment that begins with a header is parsed
12/// from the headers down. The trailing CRLF is optional too, so a bare status
13/// line parses. Fails only when the first line is neither a start line nor a
14/// header, or the input is empty.
15pub fn parse_sipfrag(data: &[u8]) -> Result<SipFragment, ParseError> {
16    if data.is_empty() {
17        return Err(ParseError::InvalidMessage("empty sipfrag".into()));
18    }
19
20    let first_line_end = CRLF.find(data).unwrap_or(data.len());
21    let mut first_line = &data[..first_line_end];
22    // A bare trailing terminator from an LF-only writer is not part of the
23    // start line; a full CRLF is already excluded by the find above.
24    if let [rest @ .., b'\n'] = first_line {
25        first_line = rest;
26    }
27    if let [rest @ .., b'\r'] = first_line {
28        first_line = rest;
29    }
30
31    let (message_type, headers_start) = match parse_first_line(first_line) {
32        Ok(mt) => (Some(mt), (first_line_end + 2).min(data.len())),
33        Err(e) => {
34            if !is_header_line(first_line) {
35                return Err(e);
36            }
37            (None, 0)
38        }
39    };
40
41    let (header_bytes, body) = split_headers_body(data, headers_start);
42
43    Ok(SipFragment {
44        message_type,
45        headers: parse_headers(header_bytes),
46        body: body.to_vec(),
47    })
48}
49
50impl SipFragment {
51    /// Content-Type with parameters stripped and lowercased, e.g.
52    /// `application/sdp` from `Application/SDP; charset=utf-8`. Use this to
53    /// dispatch on the type rather than matching the raw header value.
54    pub fn media_type(&self) -> Option<Cow<'_, str>> {
55        HasHeaders::media_type(self)
56    }
57}
58
59impl MimePart {
60    /// Parse this part's body as a `message/sipfrag` (RFC 3420).
61    ///
62    /// Does not check the Content-Type: dispatch on [`media_type`](Self::media_type)
63    /// first, then call this for the parts that claim to be fragments.
64    pub fn parse_sipfrag(&self) -> Result<SipFragment, ParseError> {
65        parse_sipfrag(&self.body)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::sip::test_support::make_multipart_invite;
73    use crate::types::SipMessageType;
74
75    #[test]
76    fn sipfrag_status_line_with_crlf() {
77        let frag = parse_sipfrag(b"SIP/2.0 200 OK\r\n").unwrap();
78        assert_eq!(
79            frag.message_type,
80            Some(SipMessageType::Response {
81                code: 200,
82                reason: "OK".into()
83            })
84        );
85        assert!(frag.headers.is_empty());
86        assert!(frag.body.is_empty());
87    }
88
89    #[test]
90    fn sipfrag_status_line_without_trailing_crlf() {
91        let frag = parse_sipfrag(b"SIP/2.0 183 Session Progress").unwrap();
92        assert_eq!(
93            frag.message_type,
94            Some(SipMessageType::Response {
95                code: 183,
96                reason: "Session Progress".into()
97            })
98        );
99    }
100
101    #[test]
102    fn sipfrag_headers_only() {
103        let frag = parse_sipfrag(b"To: <sip:user@host>\r\nCSeq: 1 INVITE\r\n").unwrap();
104        assert_eq!(frag.message_type, None);
105        assert_eq!(frag.headers.len(), 2);
106        assert_eq!(frag.headers[0].0, "To");
107        assert_eq!(frag.headers[0].1, "<sip:user@host>");
108        assert_eq!(frag.headers[1].1, "1 INVITE");
109    }
110
111    #[test]
112    fn sipfrag_header_value_is_case_insensitive() {
113        let frag = parse_sipfrag(b"To: <sip:user@host>\r\nCSeq: 1 INVITE\r\n").unwrap();
114        assert_eq!(frag.header_value("cseq"), Some("1 INVITE"));
115        assert_eq!(frag.header_value("To"), Some("<sip:user@host>"));
116        assert_eq!(frag.header_value("Call-ID"), None);
117    }
118
119    #[test]
120    fn sipfrag_headers_only_without_trailing_crlf() {
121        let frag = parse_sipfrag(b"To: <sip:user@host>").unwrap();
122        assert_eq!(frag.message_type, None);
123        assert_eq!(frag.headers.len(), 1);
124        assert_eq!(frag.headers[0].1, "<sip:user@host>");
125    }
126
127    #[test]
128    fn sipfrag_start_line_headers_and_body() {
129        let data = b"SIP/2.0 200 OK\r\n\
130            Content-Type: application/sdp\r\n\
131            \r\n\
132            v=0\r\n";
133        let frag = parse_sipfrag(data).unwrap();
134        assert_eq!(
135            frag.message_type,
136            Some(SipMessageType::Response {
137                code: 200,
138                reason: "OK".into()
139            })
140        );
141        assert_eq!(frag.headers.len(), 1);
142        assert_eq!(frag.body, b"v=0\r\n");
143    }
144
145    #[test]
146    fn sipfrag_request_start_line() {
147        let frag = parse_sipfrag(b"INVITE sip:user@host SIP/2.0\r\nCSeq: 2 INVITE\r\n").unwrap();
148        assert_eq!(
149            frag.message_type,
150            Some(SipMessageType::Request {
151                method: "INVITE".into(),
152                uri: "sip:user@host".into()
153            })
154        );
155        assert_eq!(frag.headers.len(), 1);
156    }
157
158    #[test]
159    fn sipfrag_garbage_is_error() {
160        assert!(parse_sipfrag(b"just some text without a colon").is_err());
161        assert!(parse_sipfrag(b"").is_err());
162    }
163
164    #[test]
165    fn sipfrag_content_type_and_media_type() {
166        let frag = parse_sipfrag(
167            b"SIP/2.0 200 OK\r\nContent-Type: Application/SDP; charset=utf-8\r\n\r\nv=0",
168        )
169        .unwrap();
170        assert_eq!(frag.content_type(), Some("Application/SDP; charset=utf-8"));
171        assert_eq!(frag.media_type().as_deref(), Some("application/sdp"));
172
173        let compact = parse_sipfrag(b"SIP/2.0 200 OK\r\nc: text/plain\r\n\r\nhi").unwrap();
174        assert_eq!(compact.content_type(), Some("text/plain"));
175    }
176
177    #[test]
178    fn sipfrag_malformed_start_line_with_colon_is_error() {
179        assert!(parse_sipfrag(b"INVITE sip:host SIP/1.0\r\n").is_err());
180    }
181
182    #[test]
183    fn sipfrag_lf_only_status_line() {
184        let frag = parse_sipfrag(b"SIP/2.0 200 OK\n").unwrap();
185        assert!(matches!(
186            frag.message_type,
187            Some(SipMessageType::Response { code: 200, ref reason }) if reason == "OK"
188        ));
189        assert!(frag.headers.is_empty());
190        assert!(frag.body.is_empty());
191    }
192
193    #[test]
194    fn sipfrag_from_mime_part() {
195        let body = b"SIP/2.0 100 Trying\r\n";
196        let msg = make_multipart_invite("frag-boundary", &[("message/sipfrag", body)]);
197        let parsed = msg.parse().unwrap();
198        let parts = parsed.body_as_parts();
199        assert_eq!(parts[0].media_type().as_deref(), Some("message/sipfrag"));
200
201        let frag = parts[0].parse_sipfrag().unwrap();
202        assert_eq!(
203            frag.message_type,
204            Some(SipMessageType::Response {
205                code: 100,
206                reason: "Trying".into()
207            })
208        );
209    }
210}