1use crate::header::SipHeader;
15
16fn 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
38pub 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
63const 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
90fn matches_header_name(wire_name: &str, target: &str) -> bool {
93 if wire_name.eq_ignore_ascii_case(target) {
94 return true;
95 }
96 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
118pub 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 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
173pub 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 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
217pub 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 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 #[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 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 #[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 #[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 #[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}