Skip to main content

freeswitch_sofia_trace_parser/sip/
multipart.rs

1use std::borrow::Cow;
2
3use memchr::memmem;
4
5use crate::finders::{CRLF, CRLFCRLF};
6use crate::sip::content_type::{canonical_body_header, extract_boundary, normalize_media_type};
7use crate::sip::{parse_headers, HasHeaders};
8use crate::types::{Headers, MimePart, ParsedSipMessage};
9
10#[cfg(test)]
11mod tests;
12
13pub(crate) fn is_multipart_type(content_type: Option<&str>) -> bool {
14    content_type
15        .map(|ct| normalize_media_type(ct).starts_with("multipart/"))
16        .unwrap_or(false)
17}
18
19/// A declared boundary that yields no parts is not a split: reporting it as
20/// one empty makes a body vanish from a per-part loop.
21pub(crate) fn split_multipart(content_type: Option<&str>, body: &[u8]) -> Option<Vec<MimePart>> {
22    let boundary = extract_boundary(content_type?)?;
23    let parts = parse_multipart_body(body, boundary);
24    (!parts.is_empty()).then_some(parts)
25}
26
27impl MimePart {
28    /// Content-Type with parameters stripped and lowercased, e.g.
29    /// `application/sdp` from `Application/SDP; charset=utf-8`. Use this to
30    /// dispatch on the type rather than matching the raw header value.
31    pub fn media_type(&self) -> Option<Cow<'_, str>> {
32        HasHeaders::media_type(self)
33    }
34
35    /// Returns `true` if this part's Content-Type starts with `multipart/`.
36    pub fn is_multipart(&self) -> bool {
37        HasHeaders::is_multipart(self)
38    }
39
40    /// Extract the MIME boundary string from this part's Content-Type header.
41    pub fn multipart_boundary(&self) -> Option<&str> {
42        HasHeaders::multipart_boundary(self)
43    }
44
45    /// Split a nested multipart part into its own [`MimePart`]s.
46    /// Returns `None` when this part carries no boundary or that boundary
47    /// yields no parts — either way, keep the part's own bytes.
48    ///
49    /// Descends exactly one level: a grandchild multipart comes back as a part
50    /// with its `multipart/*` type intact, to be split by another explicit
51    /// call. Depth is the caller's decision.
52    pub fn body_parts(&self) -> Option<Vec<MimePart>> {
53        HasHeaders::body_parts(self)
54    }
55}
56
57impl ParsedSipMessage {
58    /// Content-Type with parameters stripped and lowercased, e.g.
59    /// `multipart/mixed` from `multipart/mixed;boundary=abc`. Use this to
60    /// dispatch on the type rather than matching the raw header value.
61    pub fn media_type(&self) -> Option<Cow<'_, str>> {
62        HasHeaders::media_type(self)
63    }
64
65    /// Returns `true` if the Content-Type starts with `multipart/`.
66    pub fn is_multipart(&self) -> bool {
67        HasHeaders::is_multipart(self)
68    }
69
70    /// Extract the MIME boundary string from the Content-Type header.
71    pub fn multipart_boundary(&self) -> Option<&str> {
72        HasHeaders::multipart_boundary(self)
73    }
74
75    /// Split a multipart body into individual [`MimePart`]s.
76    /// Returns `None` when the Content-Type carries no `boundary` parameter or
77    /// that boundary yields no parts.
78    pub fn body_parts(&self) -> Option<Vec<MimePart>> {
79        HasHeaders::body_parts(self)
80    }
81
82    /// The body as parts, whatever its Content-Type: the multipart children
83    /// when it splits, otherwise a single part carrying the message's own
84    /// `Content-*` headers. Empty when there is no body.
85    ///
86    /// That single part is fabricated — a non-multipart body has no per-part
87    /// header block on the wire — so its headers are copied down from the
88    /// message, compact forms expanded, `Content-Length` excluded. A part
89    /// split from a real multipart body carries only what the sender wrote
90    /// there, and nothing is copied into it.
91    ///
92    /// A body that claims `multipart/*` but does not split — no boundary
93    /// parameter, or one that never appears in the body — comes back as that
94    /// one part, still typed `multipart/*`. A caller that only handles types it
95    /// recognizes then sees an unknown type rather than nothing at all.
96    ///
97    /// Descends one level only; nested multipart parts are split by calling
98    /// [`MimePart::body_parts`] on them.
99    pub fn body_as_parts(&self) -> Vec<MimePart> {
100        if self.body.is_empty() {
101            return Vec::new();
102        }
103        if let Some(parts) = self.body_parts() {
104            return parts;
105        }
106        vec![self.synthetic_part()]
107    }
108
109    /// The whole body as one part, headed by the message's own `Content-*`
110    /// headers under their canonical names.
111    fn synthetic_part(&self) -> MimePart {
112        let mut headers: Vec<(String, String)> = Vec::new();
113        if let Some(ct) = self.content_type() {
114            headers.push(("Content-Type".to_string(), ct.to_string()));
115        }
116        for (name, value) in &self.headers {
117            let Some(canonical) = canonical_body_header(name) else {
118                continue;
119            };
120            if headers
121                .iter()
122                .any(|(k, _)| k.eq_ignore_ascii_case(canonical))
123            {
124                continue;
125            }
126            headers.push((canonical.to_string(), value.clone()));
127        }
128        MimePart {
129            headers: Headers::from(headers),
130            body: self.body.clone(),
131        }
132    }
133}
134
135/// What follows a matched `--boundary` token, deciding whether the match is a
136/// real RFC 2046 delimiter line and where the next part's content starts.
137enum BoundaryTail {
138    /// Open delimiter; the value is the byte count from the end of the token
139    /// (transport padding plus CRLF) to the start of the part content.
140    Open(usize),
141    Close,
142    /// Input ends inside the delimiter line itself (truncated dump).
143    End,
144}
145
146/// Classify the bytes after a `--boundary` token. `None` means the match is
147/// not a delimiter line at all — e.g. boundary `b` matched inside `--b2`.
148fn boundary_tail(rest: &[u8]) -> Option<BoundaryTail> {
149    if rest.starts_with(b"--") {
150        return Some(BoundaryTail::Close);
151    }
152    let pad = rest
153        .iter()
154        .position(|&b| b != b' ' && b != b'\t')
155        .unwrap_or(rest.len());
156    match &rest[pad..] {
157        [] => Some(BoundaryTail::End),
158        [b'\r', b'\n', ..] => Some(BoundaryTail::Open(pad + 2)),
159        _ => None,
160    }
161}
162
163/// The `\r\n--boundary` pattern and its searcher, built once per body.
164struct BoundaryMatcher {
165    pattern: Vec<u8>,
166    finder: memmem::Finder<'static>,
167}
168
169impl BoundaryMatcher {
170    fn new(boundary: &str) -> Self {
171        let mut pattern = Vec::with_capacity(boundary.len() + 4);
172        pattern.extend_from_slice(b"\r\n--");
173        pattern.extend_from_slice(boundary.as_bytes());
174        let finder = memmem::Finder::new(&pattern).into_owned();
175        BoundaryMatcher { pattern, finder }
176    }
177
178    /// Next RFC 2046 delimiter line at or after `from`: `--boundary` at body
179    /// offset 0 (no preamble) or immediately after a CRLF. `part_end` is where
180    /// the preceding part's content stops — the CRLF belongs to the delimiter.
181    fn next_delimiter(&self, body: &[u8], from: usize) -> Option<(usize, usize, BoundaryTail)> {
182        let dash_boundary = &self.pattern[2..];
183        if from == 0 && body.starts_with(dash_boundary) {
184            if let Some(tail) = boundary_tail(&body[dash_boundary.len()..]) {
185                return Some((0, dash_boundary.len(), tail));
186            }
187        }
188        let mut search = from;
189        while let Some(rel) = self.finder.find(&body[search..]) {
190            let crlf = search + rel;
191            let token_end = crlf + 2 + dash_boundary.len();
192            if let Some(tail) = boundary_tail(&body[token_end..]) {
193                return Some((crlf, token_end, tail));
194            }
195            search = crlf + 1;
196        }
197        None
198    }
199}
200
201fn parse_multipart_body(body: &[u8], boundary: &str) -> Vec<MimePart> {
202    let matcher = BoundaryMatcher::new(boundary);
203    let mut parts = Vec::new();
204
205    let Some((_, token_end, tail)) = matcher.next_delimiter(body, 0) else {
206        return parts;
207    };
208    let mut cursor = match tail {
209        BoundaryTail::Open(skip) => token_end + skip,
210        // The body opens with the close delimiter, or truncates inside the
211        // first delimiter line: no parts.
212        BoundaryTail::Close | BoundaryTail::End => return parts,
213    };
214
215    loop {
216        match matcher.next_delimiter(body, cursor) {
217            Some((part_end, token_end, BoundaryTail::Open(skip))) => {
218                parts.push(parse_mime_part(&body[cursor..part_end]));
219                cursor = token_end + skip;
220            }
221            Some((part_end, _, BoundaryTail::Close | BoundaryTail::End)) => {
222                parts.push(parse_mime_part(&body[cursor..part_end]));
223                break;
224            }
225            // Truncated before the close delimiter: the trailing bytes are
226            // the final part, never silently dropped.
227            None => {
228                parts.push(parse_mime_part(&body[cursor..]));
229                break;
230            }
231        }
232    }
233    parts
234}
235
236fn parse_mime_part(data: &[u8]) -> MimePart {
237    match CRLFCRLF.find(data) {
238        Some(pos) => {
239            let header_bytes = &data[..pos];
240            let body = &data[pos + 4..];
241            let headers = parse_headers(header_bytes);
242            MimePart {
243                headers,
244                body: body.to_vec(),
245            }
246        }
247        None => {
248            // Could be headers-only or body-only.
249            // If first line has a colon, treat as headers with no body.
250            let first_line_end = CRLF.find(data).unwrap_or(data.len());
251            if memchr::memchr(b':', &data[..first_line_end]).is_some() {
252                let headers = parse_headers(data);
253                MimePart {
254                    headers,
255                    body: Vec::new(),
256                }
257            } else {
258                MimePart {
259                    headers: Headers::default(),
260                    body: data.to_vec(),
261                }
262            }
263        }
264    }
265}