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
118fn 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
131pub 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 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
185pub 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 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
228pub 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 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 #[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 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 #[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 #[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 #[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}