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