Skip to main content

sip_header/
message.rs

1//! SIP message text extraction utilities.
2//!
3//! Convenience functions for extracting values from raw SIP message text:
4//!
5//! - [`extract_header`] — pull header values with case-insensitive matching,
6//!   header folding (RFC 3261 §7.3.1), and compact forms (RFC 3261 §7.3.3)
7//! - [`extract_request_uri`] — pull the Request-URI from the request line
8//!   (RFC 3261 §7.1)
9//! - [`extract_body`] — pull the message body following the header block
10//!   (RFC 3261 §7.4)
11//!
12//! Gated behind the `message` feature (enabled by default).
13
14use crate::header::SipHeader;
15
16/// Split at the first empty line (after `\r` stripping) per RFC 3261 §7.3.1.
17///
18/// Returns the header block (blank line excluded) and the byte offset of
19/// the first body byte, or `None` when the message has no blank line.
20fn split_at_blank_line(message: &str) -> (&str, Option<usize>) {
21    let mut offset = 0;
22    for line in message.split('\n') {
23        let stripped = line
24            .strip_suffix('\r')
25            .unwrap_or(line);
26        if stripped.is_empty() {
27            let body_start = offset + line.len() + 1;
28            return (
29                &message[..offset],
30                (body_start <= message.len()).then_some(body_start),
31            );
32        }
33        offset += line.len() + 1;
34    }
35    (message, None)
36}
37
38/// Extract the message body — everything after the blank line that ends
39/// the header block.
40///
41/// Uses the same boundary rule as [`extract_all_headers`]: the first empty
42/// line after `\r` stripping, so bare-`\n` messages behave the same as CRLF
43/// ones. The body is returned verbatim — no trimming, unfolding, or
44/// decoding — and `Content-Length` is not consulted; the body is the rest
45/// of the given text. Returns `None` when the message has no blank line or
46/// nothing follows it.
47///
48/// # Examples
49///
50/// ```
51/// let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
52///            Content-Type: application/sdp\r\n\
53///            \r\n\
54///            v=0\r\n";
55/// assert_eq!(sip_header::extract_body(msg), Some("v=0\r\n"));
56/// ```
57pub fn extract_body(message: &str) -> Option<&str> {
58    let (_, body_start) = split_at_blank_line(message);
59    let body = &message[body_start?..];
60    (!body.is_empty()).then_some(body)
61}
62
63/// RFC 3261 §7.3.3 compact form equivalences.
64///
65/// Each pair is `(compact_char, canonical_name)`. Used by [`extract_header`]
66/// to match both compact and full header names transparently.
67const COMPACT_FORMS: &[(u8, &str)] = &[
68    (b'a', "Accept-Contact"),
69    (b'b', "Referred-By"),
70    (b'c', "Content-Type"),
71    (b'd', "Request-Disposition"),
72    (b'e', "Content-Encoding"),
73    (b'f', "From"),
74    (b'i', "Call-ID"),
75    (b'j', "Reject-Contact"),
76    (b'k', "Supported"),
77    (b'l', "Content-Length"),
78    (b'm', "Contact"),
79    (b'n', "Identity-Info"),
80    (b'o', "Event"),
81    (b'r', "Refer-To"),
82    (b's', "Subject"),
83    (b't', "To"),
84    (b'u', "Allow-Events"),
85    (b'v', "Via"),
86    (b'x', "Session-Expires"),
87    (b'y', "Identity"),
88];
89
90/// Check if a header name on the wire matches the target name, considering
91/// RFC 3261 §7.3.3 compact forms.
92fn matches_header_name(wire_name: &str, target: &str) -> bool {
93    if wire_name.eq_ignore_ascii_case(target) {
94        return true;
95    }
96    // Find the compact form equivalence for the target
97    let equiv = if target.len() == 1 {
98        let ch = target.as_bytes()[0].to_ascii_lowercase();
99        COMPACT_FORMS
100            .iter()
101            .find(|(c, _)| *c == ch)
102    } else {
103        COMPACT_FORMS
104            .iter()
105            .find(|(_, full)| full.eq_ignore_ascii_case(target))
106    };
107    if let Some(&(compact, full)) = equiv {
108        if wire_name.len() == 1 {
109            wire_name.as_bytes()[0].to_ascii_lowercase() == compact
110        } else {
111            wire_name.eq_ignore_ascii_case(full)
112        }
113    } else {
114        false
115    }
116}
117
118/// Extract all occurrences of a header from a raw SIP message.
119///
120/// Scans all lines up to the blank line separating headers from the message
121/// body. Header name matching is case-insensitive (RFC 3261 §7.3.5) and
122/// recognizes compact header forms (RFC 3261 §7.3.3): searching for `"From"`
123/// also matches `f:`, and searching for `"f"` also matches `From:`.
124///
125/// Header folding (continuation lines beginning with SP or HTAB) is unfolded
126/// into a single logical value per occurrence. Each header occurrence is
127/// returned as a separate entry — values are **not** comma-joined, per
128/// RFC 3261 §7.3.1 which forbids joining for Authorization,
129/// Proxy-Authorization, WWW-Authenticate, and Proxy-Authenticate.
130///
131/// Returns an empty `Vec` if no header with the given name is found.
132pub fn extract_header(message: &str, name: &str) -> Vec<String> {
133    let mut values: Vec<String> = Vec::new();
134    let mut current_match = false;
135    let (header_block, _) = split_at_blank_line(message);
136
137    for line in header_block.split('\n') {
138        let line = line
139            .strip_suffix('\r')
140            .unwrap_or(line);
141
142        if line.starts_with(' ') || line.starts_with('\t') {
143            if current_match {
144                if let Some(last) = values.last_mut() {
145                    last.push(' ');
146                    last.push_str(line.trim_start());
147                }
148            }
149            continue;
150        }
151
152        current_match = false;
153
154        if let Some((hdr_name, hdr_value)) = line.split_once(':') {
155            let hdr_name = hdr_name.trim_end();
156            // RFC 3261: header names are tokens — no whitespace allowed.
157            // This rejects request/status lines like "INVITE sip:..." where
158            // the text before the first colon contains spaces.
159            if !hdr_name.contains(' ') && matches_header_name(hdr_name, name) {
160                current_match = true;
161                values.push(
162                    hdr_value
163                        .trim_start()
164                        .to_string(),
165                );
166            }
167        }
168    }
169
170    values
171}
172
173/// Extract all headers from a raw SIP message as name-value pairs.
174///
175/// Returns headers in wire order, preserving multiple occurrences of the
176/// same header name as separate entries. Header folding is unfolded per
177/// RFC 3261 §7.3.1. Header names are returned as-is from the wire (not
178/// canonicalized — compact forms like `f:` remain `f`, not `From`).
179///
180/// Stops at the blank line separating headers from body.
181pub fn extract_all_headers(message: &str) -> Vec<(String, String)> {
182    let mut headers: Vec<(String, String)> = Vec::new();
183    let (header_block, _) = split_at_blank_line(message);
184
185    for line in header_block.split('\n') {
186        let line = line
187            .strip_suffix('\r')
188            .unwrap_or(line);
189
190        if line.starts_with(' ') || line.starts_with('\t') {
191            if let Some((_, value)) = headers.last_mut() {
192                value.push(' ');
193                value.push_str(line.trim_start());
194            }
195            continue;
196        }
197
198        if let Some((hdr_name, hdr_value)) = line.split_once(':') {
199            let hdr_name = hdr_name.trim_end();
200            // RFC 3261: header names are tokens — no whitespace allowed.
201            // This rejects request/status lines like "INVITE sip:..." where
202            // the text before the first colon contains spaces.
203            if !hdr_name.contains(' ') {
204                headers.push((
205                    hdr_name.to_string(),
206                    hdr_value
207                        .trim_start()
208                        .to_string(),
209                ));
210            }
211        }
212    }
213
214    headers
215}
216
217/// Extract the Request-URI from a SIP request message.
218///
219/// Parses the first line as `Method SP Request-URI SP SIP-Version`
220/// (RFC 3261 Section 7.1) and returns the Request-URI.
221///
222/// Returns `None` for status lines (`SIP/2.0 200 OK`) or if the
223/// request line cannot be parsed.
224pub fn extract_request_uri(message: &str) -> Option<String> {
225    let first_line = message
226        .lines()
227        .next()?;
228    let first_line = first_line
229        .strip_suffix('\r')
230        .unwrap_or(first_line);
231    let mut parts = first_line.split_whitespace();
232    let method = parts.next()?;
233    if method.starts_with("SIP/") {
234        return None;
235    }
236    let uri = parts.next()?;
237    let version = parts.next()?;
238    if parts
239        .next()
240        .is_some()
241    {
242        return None;
243    }
244    if !version.starts_with("SIP/") {
245        return None;
246    }
247    Some(uri.to_string())
248}
249
250impl SipHeader {
251    /// Extract all occurrences of this header from a raw SIP message.
252    ///
253    /// Recognizes both the canonical header name and its compact form
254    /// (RFC 3261 §7.3.3). For example, `SipHeader::From.extract_from(msg)`
255    /// matches both `From:` and `f:` lines.
256    pub fn extract_from(&self, message: &str) -> Vec<String> {
257        extract_header(message, self.as_str())
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    const SAMPLE_INVITE: &str = "\
266INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
267Via: SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds\r\n\
268Via: SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8\r\n\
269Max-Forwards: 70\r\n\
270To: Bob <sip:bob@biloxi.example.com>\r\n\
271From: Alice <sip:alice@atlanta.example.com>;tag=1928301774\r\n\
272Call-ID: a84b4c76e66710@pc33.atlanta.example.com\r\n\
273CSeq: 314159 INVITE\r\n\
274Contact: <sip:alice@pc33.atlanta.example.com>\r\n\
275Content-Type: application/sdp\r\n\
276Content-Length: 142\r\n\
277\r\n\
278v=0\r\n\
279o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n";
280
281    #[test]
282    fn basic_extraction() {
283        let from = extract_header(SAMPLE_INVITE, "From");
284        assert_eq!(from.len(), 1);
285        assert_eq!(
286            from[0],
287            "Alice <sip:alice@atlanta.example.com>;tag=1928301774"
288        );
289
290        let call_id = extract_header(SAMPLE_INVITE, "Call-ID");
291        assert_eq!(call_id.len(), 1);
292        assert_eq!(call_id[0], "a84b4c76e66710@pc33.atlanta.example.com");
293
294        let cseq = extract_header(SAMPLE_INVITE, "CSeq");
295        assert_eq!(cseq.len(), 1);
296        assert_eq!(cseq[0], "314159 INVITE");
297    }
298
299    #[test]
300    fn case_insensitive_name() {
301        let expected = "Alice <sip:alice@atlanta.example.com>;tag=1928301774";
302        assert_eq!(extract_header(SAMPLE_INVITE, "from")[0], expected);
303        assert_eq!(extract_header(SAMPLE_INVITE, "FROM")[0], expected);
304        assert_eq!(extract_header(SAMPLE_INVITE, "From")[0], expected);
305    }
306
307    #[test]
308    fn header_folding() {
309        let msg = concat!(
310            "SIP/2.0 200 OK\r\n",
311            "Subject: I know you're there,\r\n",
312            " pick up the phone\r\n",
313            " and talk to me!\r\n",
314            "\r\n",
315        );
316        let result = extract_header(msg, "Subject");
317        assert_eq!(result.len(), 1);
318        assert_eq!(
319            result[0],
320            "I know you're there, pick up the phone and talk to me!"
321        );
322    }
323
324    #[test]
325    fn multiple_occurrences_separate() {
326        let via = extract_header(SAMPLE_INVITE, "Via");
327        assert_eq!(via.len(), 2);
328        assert_eq!(
329            via[0],
330            "SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds"
331        );
332        assert_eq!(
333            via[1],
334            "SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8"
335        );
336    }
337
338    #[test]
339    fn stops_at_blank_line() {
340        assert!(extract_header(SAMPLE_INVITE, "o").is_empty());
341    }
342
343    #[test]
344    fn bare_lf_line_endings() {
345        let msg = "SIP/2.0 200 OK\n\
346                   From: Alice <sip:alice@host>\n\
347                   To: Bob <sip:bob@host>\n\
348                   \n\
349                   body\n";
350        let from = extract_header(msg, "From");
351        assert_eq!(from.len(), 1);
352        assert_eq!(from[0], "Alice <sip:alice@host>");
353    }
354
355    #[test]
356    fn missing_header_returns_empty() {
357        assert!(extract_header(SAMPLE_INVITE, "X-Custom").is_empty());
358    }
359
360    #[test]
361    fn empty_message() {
362        assert!(extract_header("", "From").is_empty());
363    }
364
365    #[test]
366    fn request_line_not_matched() {
367        assert!(extract_header(SAMPLE_INVITE, "INVITE sip").is_empty());
368    }
369
370    #[test]
371    fn value_leading_whitespace_trimmed() {
372        let msg = "SIP/2.0 200 OK\r\n\
373                   From:   Alice <sip:alice@host>\r\n\
374                   \r\n";
375        let from = extract_header(msg, "From");
376        assert_eq!(from.len(), 1);
377        assert_eq!(from[0], "Alice <sip:alice@host>");
378    }
379
380    #[test]
381    fn folding_on_multiple_occurrence() {
382        let msg = concat!(
383            "SIP/2.0 200 OK\r\n",
384            "Via: SIP/2.0/UDP first.example.com\r\n",
385            " ;branch=z9hG4bKaaa\r\n",
386            "Via: SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb\r\n",
387            "\r\n",
388        );
389        let via = extract_header(msg, "Via");
390        assert_eq!(via.len(), 2);
391        assert_eq!(via[0], "SIP/2.0/UDP first.example.com ;branch=z9hG4bKaaa");
392        assert_eq!(via[1], "SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb");
393    }
394
395    #[test]
396    fn empty_header_value() {
397        let msg = "SIP/2.0 200 OK\r\n\
398                   Subject:\r\n\
399                   From: Alice <sip:alice@host>\r\n\
400                   \r\n";
401        let subject = extract_header(msg, "Subject");
402        assert_eq!(subject.len(), 1);
403        assert_eq!(subject[0], "");
404    }
405
406    #[test]
407    fn tab_folding() {
408        let msg = concat!(
409            "SIP/2.0 200 OK\r\n",
410            "Subject: hello\r\n",
411            "\tworld\r\n",
412            "\r\n",
413        );
414        let subject = extract_header(msg, "Subject");
415        assert_eq!(subject.len(), 1);
416        assert_eq!(subject[0], "hello world");
417    }
418
419    // -- Compact form tests (RFC 3261 §7.3.3) --
420
421    #[test]
422    fn compact_form_from() {
423        let msg = "SIP/2.0 200 OK\r\nf: Alice <sip:alice@host>\r\n\r\n";
424        assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
425        assert_eq!(extract_header(msg, "f")[0], "Alice <sip:alice@host>");
426    }
427
428    #[test]
429    fn compact_form_via() {
430        let msg = "SIP/2.0 200 OK\r\nv: SIP/2.0/UDP host\r\n\r\n";
431        assert_eq!(extract_header(msg, "Via")[0], "SIP/2.0/UDP host");
432        assert_eq!(extract_header(msg, "v")[0], "SIP/2.0/UDP host");
433    }
434
435    #[test]
436    fn compact_form_mixed_with_full() {
437        let msg = concat!(
438            "SIP/2.0 200 OK\r\n",
439            "f: Alice <sip:alice@host>;tag=a\r\n",
440            "t: Bob <sip:bob@host>;tag=b\r\n",
441            "i: call-1@host\r\n",
442            "m: <sip:alice@192.0.2.1>\r\n",
443            "Content-Type: application/sdp\r\n",
444            "\r\n",
445        );
446        assert_eq!(
447            extract_header(msg, "From")[0],
448            "Alice <sip:alice@host>;tag=a"
449        );
450        assert_eq!(extract_header(msg, "To")[0], "Bob <sip:bob@host>;tag=b");
451        assert_eq!(extract_header(msg, "Call-ID")[0], "call-1@host");
452        assert_eq!(extract_header(msg, "Contact")[0], "<sip:alice@192.0.2.1>");
453        assert_eq!(extract_header(msg, "Content-Type")[0], "application/sdp");
454        assert_eq!(extract_header(msg, "c")[0], "application/sdp");
455    }
456
457    #[test]
458    fn compact_form_case_insensitive() {
459        let msg = "SIP/2.0 200 OK\r\nF: Alice <sip:alice@host>\r\n\r\n";
460        assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
461    }
462
463    #[test]
464    fn compact_form_unknown_single_char() {
465        let msg = "SIP/2.0 200 OK\r\nz: something\r\n\r\n";
466        assert_eq!(extract_header(msg, "z")[0], "something");
467        assert!(extract_header(msg, "From").is_empty());
468    }
469
470    // -- Integration pipeline tests: extract_header → existing parsers --
471
472    const NG911_INVITE: &str = concat!(
473        "INVITE sip:urn:service:sos@bcf.example.com SIP/2.0\r\n",
474        "Via: SIP/2.0/TLS proxy.example.com;branch=z9hG4bK776\r\n",
475        "From: \"Caller Name\" <sip:+15551234567@orig.example.com>;tag=abc123\r\n",
476        "To: <sip:urn:service:sos@bcf.example.com>\r\n",
477        "Call-ID: ng911-call-42@orig.example.com\r\n",
478        "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
479        "Call-Info: <urn:emergency:uid:callid:abc:bcf.example.com>;purpose=emergency-CallId,",
480        "<https://adr.example.com/serviceInfo?t=x>;purpose=EmergencyCallData.ServiceInfo\r\n",
481        "Geolocation: <cid:loc-id-1234>, <https://lis.example.com/held/test>\r\n",
482        "Content-Type: application/sdp\r\n",
483        "\r\n",
484        "v=0\r\n",
485    );
486
487    #[test]
488    fn extract_and_parse_call_info() {
489        use crate::uri_info::UriInfo;
490
491        let raw = extract_header(NG911_INVITE, "Call-Info");
492        assert_eq!(raw.len(), 1);
493        let ci = UriInfo::parse(&raw[0]).unwrap();
494        assert_eq!(ci.len(), 2);
495        assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
496        assert!(ci
497            .entries()
498            .iter()
499            .any(|e| e.purpose() == Some("EmergencyCallData.ServiceInfo")));
500    }
501
502    #[test]
503    fn extract_and_parse_p_asserted_identity() {
504        use crate::header_addr::SipHeaderAddr;
505
506        let raw = extract_header(NG911_INVITE, "P-Asserted-Identity");
507        assert_eq!(raw.len(), 1);
508        let pai: SipHeaderAddr = raw[0]
509            .parse()
510            .unwrap();
511        assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
512        assert!(pai
513            .uri()
514            .to_string()
515            .contains("+15551234567"));
516    }
517
518    #[test]
519    fn extract_and_parse_multi_pai() {
520        use crate::header_addr::SipHeaderAddr;
521
522        let msg = concat!(
523            "INVITE sip:sos@psap.example.com SIP/2.0\r\n",
524            "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
525            "P-Asserted-Identity: <tel:+15551234567>\r\n",
526            "\r\n",
527        );
528        let raw = extract_header(msg, "P-Asserted-Identity");
529        assert_eq!(raw.len(), 2);
530        let pai0: SipHeaderAddr = raw[0]
531            .parse()
532            .unwrap();
533        assert_eq!(pai0.display_name(), Some("EXAMPLE CO"));
534        let pai1: SipHeaderAddr = raw[1]
535            .parse()
536            .unwrap();
537        assert!(pai1
538            .uri()
539            .to_string()
540            .contains("+15551234567"));
541    }
542
543    #[test]
544    fn extract_and_parse_geolocation() {
545        use crate::geolocation::SipGeolocation;
546
547        let raw = extract_header(NG911_INVITE, "Geolocation");
548        assert_eq!(raw.len(), 1);
549        let geo = SipGeolocation::parse(&raw[0]);
550        assert_eq!(geo.len(), 2);
551        assert_eq!(geo.cid(), Some("loc-id-1234"));
552        assert!(geo
553            .url()
554            .unwrap()
555            .contains("lis.example.com"));
556    }
557
558    #[test]
559    fn extract_and_parse_from_to() {
560        use crate::header_addr::SipHeaderAddr;
561
562        let from_raw = extract_header(NG911_INVITE, "From");
563        assert_eq!(from_raw.len(), 1);
564        let from: SipHeaderAddr = from_raw[0]
565            .parse()
566            .unwrap();
567        assert_eq!(from.display_name(), Some("Caller Name"));
568        assert_eq!(from.tag(), Some("abc123"));
569
570        let to_raw = extract_header(NG911_INVITE, "To");
571        assert_eq!(to_raw.len(), 1);
572        let to: SipHeaderAddr = to_raw[0]
573            .parse()
574            .unwrap();
575        assert!(to
576            .uri()
577            .to_string()
578            .contains("urn:service:sos"));
579    }
580
581    // -- extract_request_uri tests (RFC 3261 §7.1) --
582
583    #[test]
584    fn extract_request_uri_invite() {
585        let msg = "INVITE urn:service:sos SIP/2.0\r\nTo: <urn:service:sos>\r\n\r\n";
586        assert_eq!(extract_request_uri(msg), Some("urn:service:sos".into()));
587    }
588
589    #[test]
590    fn extract_request_uri_sip() {
591        let msg = "INVITE sip:+15550001234@198.51.100.1:5060 SIP/2.0\r\n\r\n";
592        assert_eq!(
593            extract_request_uri(msg),
594            Some("sip:+15550001234@198.51.100.1:5060".into()),
595        );
596    }
597
598    #[test]
599    fn extract_request_uri_status_line() {
600        let msg = "SIP/2.0 200 OK\r\n\r\n";
601        assert_eq!(extract_request_uri(msg), None);
602    }
603
604    #[test]
605    fn extract_request_uri_empty() {
606        assert_eq!(extract_request_uri(""), None);
607    }
608
609    // -- extract_all_headers tests --
610
611    #[test]
612    fn extract_all_headers_basic() {
613        let msg = concat!(
614            "SIP/2.0 200 OK\r\n",
615            "Via: SIP/2.0/UDP host\r\n",
616            "From: Alice <sip:alice@example.com>\r\n",
617            "To: Bob <sip:bob@example.com>\r\n",
618            "\r\n",
619        );
620        let headers = extract_all_headers(msg);
621        assert_eq!(headers.len(), 3);
622        assert_eq!(headers[0], ("Via".into(), "SIP/2.0/UDP host".into()));
623        assert_eq!(
624            headers[1],
625            ("From".into(), "Alice <sip:alice@example.com>".into())
626        );
627        assert_eq!(
628            headers[2],
629            ("To".into(), "Bob <sip:bob@example.com>".into())
630        );
631    }
632
633    #[test]
634    fn extract_all_headers_folding() {
635        let msg = concat!(
636            "SIP/2.0 200 OK\r\n",
637            "Subject: I know you're there,\r\n",
638            " pick up the phone\r\n",
639            " and talk to me!\r\n",
640            "From: Alice <sip:alice@example.com>\r\n",
641            "\r\n",
642        );
643        let headers = extract_all_headers(msg);
644        assert_eq!(headers.len(), 2);
645        assert_eq!(
646            headers[0].1,
647            "I know you're there, pick up the phone and talk to me!"
648        );
649    }
650
651    #[test]
652    fn extract_all_headers_compact_forms_verbatim() {
653        let msg = concat!(
654            "SIP/2.0 200 OK\r\n",
655            "f: Alice <sip:alice@example.com>\r\n",
656            "t: Bob <sip:bob@example.com>\r\n",
657            "i: call-1@host\r\n",
658            "\r\n",
659        );
660        let headers = extract_all_headers(msg);
661        assert_eq!(headers.len(), 3);
662        assert_eq!(headers[0].0, "f");
663        assert_eq!(headers[1].0, "t");
664        assert_eq!(headers[2].0, "i");
665    }
666
667    #[test]
668    fn extract_all_headers_stops_at_blank_line() {
669        let msg = concat!(
670            "INVITE sip:bob@example.com SIP/2.0\r\n",
671            "From: Alice <sip:alice@example.com>\r\n",
672            "\r\n",
673            "v=0\r\n",
674            "o=alice 123 456 IN IP4 198.51.100.1\r\n",
675        );
676        let headers = extract_all_headers(msg);
677        assert_eq!(headers.len(), 1);
678        assert_eq!(headers[0].0, "From");
679    }
680
681    #[test]
682    fn extract_all_headers_multiple_same_name() {
683        let msg = concat!(
684            "SIP/2.0 200 OK\r\n",
685            "Via: SIP/2.0/UDP first.example.com\r\n",
686            "Via: SIP/2.0/UDP second.example.com\r\n",
687            "\r\n",
688        );
689        let headers = extract_all_headers(msg);
690        assert_eq!(headers.len(), 2);
691        assert_eq!(
692            headers[0],
693            ("Via".into(), "SIP/2.0/UDP first.example.com".into())
694        );
695        assert_eq!(
696            headers[1],
697            ("Via".into(), "SIP/2.0/UDP second.example.com".into())
698        );
699    }
700
701    #[test]
702    fn extract_all_headers_empty_message() {
703        assert!(extract_all_headers("").is_empty());
704    }
705
706    #[test]
707    fn extract_all_headers_skips_request_line() {
708        let msg = concat!(
709            "INVITE sip:bob@example.com SIP/2.0\r\n",
710            "From: Alice <sip:alice@example.com>\r\n",
711            "\r\n",
712        );
713        let headers = extract_all_headers(msg);
714        assert_eq!(headers.len(), 1);
715        assert_eq!(headers[0].0, "From");
716    }
717
718    #[test]
719    fn extract_all_headers_skips_status_line() {
720        let msg = concat!(
721            "SIP/2.0 200 OK\r\n",
722            "From: Alice <sip:alice@example.com>\r\n",
723            "\r\n",
724        );
725        let headers = extract_all_headers(msg);
726        assert_eq!(headers.len(), 1);
727        assert_eq!(headers[0].0, "From");
728    }
729
730    #[test]
731    fn extract_all_headers_tab_folding() {
732        let msg = concat!(
733            "SIP/2.0 200 OK\r\n",
734            "Subject: hello\r\n",
735            "\tworld\r\n",
736            "\r\n",
737        );
738        let headers = extract_all_headers(msg);
739        assert_eq!(headers.len(), 1);
740        assert_eq!(headers[0].1, "hello world");
741    }
742
743    #[test]
744    fn extract_all_headers_empty_value() {
745        let msg = concat!(
746            "SIP/2.0 200 OK\r\n",
747            "Subject:\r\n",
748            "From: Alice <sip:alice@example.com>\r\n",
749            "\r\n",
750        );
751        let headers = extract_all_headers(msg);
752        assert_eq!(headers.len(), 2);
753        assert_eq!(headers[0], ("Subject".into(), "".into()));
754    }
755
756    // -- extract_body tests --
757
758    #[test]
759    fn extract_body_crlf() {
760        assert_eq!(
761            extract_body(SAMPLE_INVITE),
762            Some("v=0\r\no=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n")
763        );
764    }
765
766    #[test]
767    fn extract_body_bare_lf() {
768        let msg = "SIP/2.0 200 OK\n\
769                   From: Alice <sip:alice@host>\n\
770                   \n\
771                   v=0\no=alice 123 456 IN IP4 198.51.100.1\n";
772        assert_eq!(
773            extract_body(msg),
774            Some("v=0\no=alice 123 456 IN IP4 198.51.100.1\n")
775        );
776    }
777
778    #[test]
779    fn extract_body_folded_headers() {
780        let msg = concat!(
781            "SIP/2.0 200 OK\r\n",
782            "Subject: I know you're there,\r\n",
783            " pick up the phone\r\n",
784            "\r\n",
785            "body text\r\n",
786        );
787        assert_eq!(extract_body(msg), Some("body text\r\n"));
788    }
789
790    #[test]
791    fn extract_body_no_blank_line() {
792        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
793                   From: Alice <sip:alice@example.com>\r\n";
794        assert_eq!(extract_body(msg), None);
795    }
796
797    #[test]
798    fn extract_body_blank_line_nothing_after() {
799        let msg = "SIP/2.0 200 OK\r\n\
800                   From: Alice <sip:alice@host>\r\n\
801                   \r\n";
802        assert_eq!(extract_body(msg), None);
803    }
804
805    #[test]
806    fn extract_body_empty_message() {
807        assert_eq!(extract_body(""), None);
808    }
809
810    #[test]
811    fn extract_body_multipart_blank_lines_kept() {
812        let msg = concat!(
813            "INVITE sip:sos@psap.example.com SIP/2.0\r\n",
814            "Content-Type: multipart/mixed;boundary=b1\r\n",
815            "\r\n",
816            "--b1\r\n",
817            "Content-Type: application/sdp\r\n",
818            "\r\n",
819            "v=0\r\n",
820            "--b1--\r\n",
821        );
822        assert_eq!(
823            extract_body(msg),
824            Some("--b1\r\nContent-Type: application/sdp\r\n\r\nv=0\r\n--b1--\r\n")
825        );
826    }
827
828    #[test]
829    fn extract_body_header_looking_line_in_body() {
830        let msg = "SIP/2.0 200 OK\r\n\
831                   \r\n\
832                   From: not a header\r\n";
833        assert_eq!(extract_body(msg), Some("From: not a header\r\n"));
834    }
835
836    #[test]
837    fn extract_body_after_request_line() {
838        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
839                   \r\n\
840                   v=0\r\n";
841        assert_eq!(extract_body(msg), Some("v=0\r\n"));
842    }
843
844    #[test]
845    fn extract_all_headers_bare_lf() {
846        let msg = "SIP/2.0 200 OK\n\
847                   From: Alice <sip:alice@example.com>\n\
848                   \n\
849                   body\n";
850        let headers = extract_all_headers(msg);
851        assert_eq!(headers.len(), 1);
852        assert_eq!(
853            headers[0],
854            ("From".into(), "Alice <sip:alice@example.com>".into())
855        );
856    }
857}