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