├── .gitattributes ├── .gitignore ├── README.md ├── tests ├── requests │ ├── escnull.dat │ ├── esc02.dat │ ├── esc01.dat │ ├── intmeth.dat │ └── wsinv.dat └── requests.rs ├── src ├── lib.rs ├── lookup.rs └── sip.rs ├── Cargo.toml ├── LICENSE ├── benches └── parse.rs ├── .travis.yml └── scripts └── lookup_table_generator.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.rs text eol=lf 3 | *.dat binary 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parsip 2 | 3 | [![Build Status](https://travis-ci.org/kamarkiewicz/parsip.svg?branch=master)](https://travis-ci.org/kamarkiewicz/parsip) 4 | [![codecov](https://codecov.io/gh/kamarkiewicz/parsip/branch/master/graph/badge.svg)](https://codecov.io/gh/kamarkiewicz/parsip) 5 | [![crates.io](https://img.shields.io/crates/v/parsip.svg?maxAge=2592000)](https://crates.io/crates/parsip) 6 | -------------------------------------------------------------------------------- /tests/requests/escnull.dat: -------------------------------------------------------------------------------- 1 | REGISTER sip:example.com SIP/2.0 2 | To: sip:null-%00-null@example.com 3 | From: sip:null-%00-null@example.com;tag=839923423 4 | Max-Forwards: 70 5 | Call-ID: escnull.39203ndfvkjdasfkq3w4otrq0adsfdfnavd 6 | CSeq: 14398234 REGISTER 7 | Via: SIP/2.0/UDP host5.example.com;rport;branch=z9hG4bKkdjuw 8 | Contact: 9 | Contact: 10 | L:0 11 | 12 | -------------------------------------------------------------------------------- /tests/requests/esc02.dat: -------------------------------------------------------------------------------- 1 | RE%47IST%45R sip:registrar.example.com SIP/2.0 2 | To: "%Z%45" 3 | From: "%Z%45" ;tag=f232jadfj23 4 | Call-ID: esc02.asdfnqwo34rq23i34jrjasdcnl23nrlknsdf 5 | Via: SIP/2.0/TCP host.example.com;rport;branch=z9hG4bK209%fzsnel234 6 | CSeq: 29344 RE%47IST%45R 7 | Max-Forwards: 70 8 | Contact: 9 | C%6Fntact: 10 | Contact: 11 | l: 0 12 | 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | // FIXME: unnecessary parentheses around function argument caused by do_parse! 3 | // #![cfg_attr(test, deny(warnings))] 4 | #![deny(missing_docs)] 5 | #![deny(dead_code)] 6 | //! # parsip 7 | //! 8 | //! A push library for parsing SIP requests and responses. 9 | //! 10 | 11 | #[macro_use] 12 | extern crate nom; 13 | 14 | #[cfg(not(feature = "std"))] 15 | mod std { 16 | pub use core::*; 17 | } 18 | 19 | mod sip; 20 | mod lookup; 21 | 22 | pub use sip::*; 23 | -------------------------------------------------------------------------------- /tests/requests/esc01.dat: -------------------------------------------------------------------------------- 1 | INVITE sip:sips%3Auser%40example.com@example.net SIP/2.0 2 | To: sip:%75se%72@example.com 3 | From: ;tag=$FROM_TAG 4 | Max-Forwards: 87 5 | i: esc01.239409asdfakjkn23onasd0-3234 6 | CSeq: 234234 INVITE 7 | Via: SIP/2.0/UDP host5.example.net;rport;branch=z9hG4bKkdjuw 8 | C: application/sdp 9 | Contact: 10 | 11 | Content-Length: 150 12 | 13 | v=0 14 | o=mhandley 29739 7272939 IN IP4 192.0.2.1 15 | s=- 16 | c=IN IP4 192.0.2.1 17 | t=0 0 18 | m=audio 49217 RTP/AVP 0 12 19 | m=video 3227 RTP/AVP 31 20 | a=rtpmap:31 LPC 21 | -------------------------------------------------------------------------------- /tests/requests/intmeth.dat: -------------------------------------------------------------------------------- 1 | !interesting-Method0123456789_*+`.%indeed'~ sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*:&it+has=1,weird!*pas$wo~d_too.(doesn't-it)@example.com SIP/2.0 2 | Via: SIP/2.0/UDP host1.example.com;rport;branch=z9hG4bK-.!%66*_+`'~ 3 | To: "BEL:\ NUL:\ DEL:\" 4 | From: token1~` token2'+_ token3*%!.- ;fromParam''~+*_!.-%="работающий";tag=_token~1'+`*%!-. 5 | Call-ID: intmeth.word%ZK-!.*_+'@word`~)(><:\/"][?}{ 6 | CSeq: 139122385 !interesting-Method0123456789_*+`.%indeed'~ 7 | Max-Forwards: 255 8 | extensionHeader-!.%*+_`'~: 大停電 9 | Content-Length: 0 10 | 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parsip" 3 | version = "0.1.0" 4 | authors = ["Kamil Markiewicz "] 5 | license = "MIT" 6 | description = "A tiny, safe, speedy, zero-copy SIP parser. Uses nom." 7 | repository = "https://github.com/kamarkiewicz/parsip" 8 | documentation = "https://docs.rs/parsip" 9 | keywords = ["sip", "parser"] 10 | categories = ["network-programming", "parser-implementations"] 11 | 12 | [features] 13 | default = ["std"] 14 | std = ["nom/std"] 15 | 16 | [dependencies.nom] 17 | version = "^3.1" 18 | default-features = false 19 | 20 | [dev-dependencies] 21 | bencher = "^0.1" 22 | 23 | [[bench]] 24 | name = "parse" 25 | harness = false 26 | 27 | [profile.bench] 28 | lto = true 29 | codegen-units = 1 30 | opt-level = 3 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kamil Markiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benches/parse.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate bencher; 3 | 4 | use bencher::Bencher; 5 | 6 | extern crate parsip; 7 | 8 | const REQ: &'static [u8] = b"\ 9 | INVITE sip:bob@biloxi.com SIP/2.0\r\n\ 10 | Via: SIP/2.0/UDP bigbox3.site3.atlanta.com;branch=z9hG4bK77ef4c2312983.1\r\n\ 11 | Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKnashds8;received=192.0.2.1\r\n\ 12 | Max-Forwards: 69\r\n\ 13 | To: Bob \r\n\ 14 | From: Alice ;tag=1928301774\r\n\ 15 | Call-ID: a84b4c76e66710\r\n\ 16 | CSeq: 314159 INVITE\r\n\ 17 | Contact: \r\n\ 18 | Content-Type: application/sdp\r\n\ 19 | Content-Length: 0\r\n\r\n"; 20 | 21 | 22 | 23 | fn bench_parsip_request(b: &mut Bencher) { 24 | let mut headers = [parsip::Header { 25 | name: "", 26 | value: &[], 27 | }; 16]; 28 | let mut req = parsip::Request::new(&mut headers); 29 | b.iter(|| { 30 | assert_eq!(req.parse(REQ), parsip::IResult::Done(&b""[..], REQ.len())); 31 | }); 32 | b.bytes = REQ.len() as u64; 33 | } 34 | 35 | benchmark_group!(benches, bench_parsip_request); 36 | benchmark_main!(benches); 37 | -------------------------------------------------------------------------------- /tests/requests/wsinv.dat: -------------------------------------------------------------------------------- 1 | INVITE sip:vivekg@chair-dnrc.example.com;unknownparam SIP/2.0 2 | TO : 3 | sip:vivekg@chair-dnrc.example.com ; tag = 1918181833n 4 | from : "J Rosenberg \\\"" 5 | ; 6 | tag = 98asjd8 7 | MaX-fOrWaRdS: 0068 8 | Call-ID: wsinv.ndaksdj@192.0.2.1 9 | Content-Length : 150 10 | cseq: 0009 11 | INVITE 12 | Via : SIP / 2.0 13 | /UDP 14 | 192.0.2.2;rport;branch=390skdjuw 15 | s : 16 | NewFangledHeader: newfangled value 17 | continued newfangled value 18 | UnknownHeaderWithUnusualValue: ;;,,;;,; 19 | Content-Type: application/sdp 20 | Route: 21 | 22 | v: SIP / 2.0 / TCP spindle.example.com ; 23 | branch = z9hG4bK9ikj8 , 24 | SIP / 2.0 / UDP 192.168.255.111 ; branch= 25 | z9hG4bK30239 26 | m:"Quoted string \"\"" ; newparam = 27 | newvalue ; 28 | secondparam ; q = 0.33 29 | 30 | v=0 31 | o=mhandley 29739 7272939 IN IP4 192.0.2.3 32 | s=- 33 | c=IN IP4 192.0.2.4 34 | t=0 0 35 | m=audio 49217 RTP/AVP 0 12 36 | m=video 3227 RTP/AVP 31 37 | a=rtpmap:31 LPC 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: required 3 | dist: trusty 4 | 5 | rust: 6 | - nightly 7 | - beta 8 | - stable 9 | matrix: 10 | fast_finish: true 11 | 12 | 13 | cache: 14 | apt: true 15 | cargo: true 16 | before_cache: 17 | # Travis can't cache files that are not readable by "others" 18 | - chmod -R a+r $HOME/.cargo 19 | 20 | # Dependencies of kcov, used by coverage 21 | addons: 22 | apt: 23 | packages: 24 | - libcurl4-openssl-dev 25 | - libelf-dev 26 | - libdw-dev 27 | - binutils-dev 28 | - cmake # also required for cargo-update 29 | sources: 30 | - kalakris-cmake 31 | 32 | before_script: 33 | - export PATH=$HOME/.cargo/bin:$PATH 34 | - which cargo-install-update || cargo install cargo-update 35 | - which cargo-benchcmp || cargo install cargo-benchcmp 36 | - which cargo-coverage || cargo install cargo-travis 37 | - cargo install-update -a # update outdated cached binaries 38 | 39 | script: 40 | - cargo build --verbose 41 | - cargo test --verbose 42 | - cargo bench --verbose 43 | - | 44 | set -e 45 | if [ "${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" != "master" ]; then 46 | cargo bench --verbose | tee benches-variable 47 | git config remote.origin.fetch "+refs/heads/master:refs/remotes/origin/master" 48 | git remote update 49 | git checkout -b master origin/master 50 | cargo bench --verbose | tee benches-control 51 | cargo benchcmp benches-control benches-variable 52 | fi 53 | 54 | after_success: 55 | # measure code coverage and upload to codecov.io 56 | - cargo coverage -m ../kcov && bash <(curl -s https://codecov.io/bash) 57 | -------------------------------------------------------------------------------- /scripts/lookup_table_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | def generate_lookup_table(name, func, n=256, align=16): 5 | assert(isinstance(n, int)) 6 | FIRST_LINE = 'static {}: [bool; {}] = byte_map!['.format(name, n) 7 | LAST_LINE = '];' 8 | print(FIRST_LINE, end='') 9 | for i in range(n): 10 | if i % align == 0: print('\n ', end='') 11 | print(' {},'.format(1 if func(i) else 0), end='') 12 | print('\n' + LAST_LINE + '\n') 13 | 14 | 15 | 16 | def is_alphanum(b): 17 | return (ord(b'a') <= b <= ord(b'z')) or \ 18 | (ord(b'A') <= b <= ord(b'Z')) or \ 19 | (ord(b'0') <= b <= ord(b'9')) 20 | 21 | 22 | ''' RFC 3261 23 | token = 1*(alphanum / "-" / "." / "!" / "%" / "*" 24 | / "_" / "+" / "`" / "'" / "~" ) 25 | ''' 26 | def is_token(b): 27 | return is_alphanum(b) or \ 28 | (b in b"!%'*+-._`~") 29 | 30 | 31 | 32 | ''' RFC 3261 33 | Request-URI = SIP-URI / SIPS-URI / absoluteURI 34 | absoluteURI = scheme ":" ( hier-part / opaque-part ) 35 | hier-part = ( net-path / abs-path ) [ "?" query ] 36 | net-path = "//" authority [ abs-path ] 37 | abs-path = "/" path-segments 38 | SIP-URI = "sip:" [ userinfo ] hostport 39 | uri-parameters [ headers ] 40 | SIPS-URI = "sips:" [ userinfo ] hostport 41 | uri-parameters [ headers ] 42 | userinfo = ( user / telephone-subscriber ) [ ":" password ] "@" 43 | user = 1*( unreserved / escaped / user-unreserved ) 44 | user-unreserved = "&" / "=" / "+" / "$" / "," / ";" / "?" / "/" 45 | password = *( unreserved / escaped / 46 | "&" / "=" / "+" / "$" / "," ) 47 | hostport = host [ ":" port ] 48 | host = hostname / IPv4address / IPv6reference 49 | hostname = *( domainlabel "." ) toplabel [ "." ] 50 | domainlabel = alphanum 51 | / alphanum *( alphanum / "-" ) alphanum 52 | toplabel = ALPHA / ALPHA *( alphanum / "-" ) alphanum 53 | ''' 54 | def is_request_uri(b): 55 | return is_alphanum(b) or \ 56 | (b in b"!$%&'()*+,-./:;=?@_~") 57 | 58 | 59 | ''' RFC 3261 60 | Reason-Phrase = *(reserved / unreserved / escaped 61 | / UTF8-NONASCII / UTF8-CONT / SP / HTAB) 62 | ''' 63 | def is_reason_phrase(b): 64 | return is_alphanum(b) or \ 65 | (b in b"!$%&'()*+,-./:;=?@_~") or \ 66 | (b in b"\xff") or \ 67 | (b in b" \t") 68 | 69 | 70 | ''' 71 | TEXT-UTF8char = %x21-7E / UTF8-NONASCII 72 | ''' 73 | def is_TEXT_UTF8char(b): 74 | return (ord(b'\x21') <= b <= ord(b'\x7E')) or \ 75 | is_UTF8_NONASCII(b) 76 | 77 | ''' 78 | UTF8-NONASCII = %xC0-DF 1UTF8-CONT 79 | / %xE0-EF 2UTF8-CONT 80 | / %xF0-F7 3UTF8-CONT 81 | / %xF8-Fb 4UTF8-CONT 82 | / %xFC-FD 5UTF8-CONT 83 | ''' 84 | def is_UTF8_NONASCII(b): 85 | return (ord(b'\xC0') <= b <= ord(b'\xDF')) or \ 86 | (ord(b'\xE0') <= b <= ord(b'\xEF')) or \ 87 | (ord(b'\xF0') <= b <= ord(b'\xF7')) or \ 88 | (ord(b'\xF8') <= b <= ord(b'\xFb')) or \ 89 | (ord(b'\xFC') <= b <= ord(b'\xFD')) 90 | 91 | ''' 92 | UTF8-CONT = %x80-BF 93 | ''' 94 | def is_UTF8_CONT(b): 95 | return (ord(b'\x80') <= b <= ord(b'\xBF')) 96 | 97 | ''' 98 | Where ([THIS](https://www.rfc-editor.org/std/std68.txt)): 99 | 100 | LWS = [*WSP CRLF] 1*WSP ; linear whitespace 101 | WSP = SP / HTAB ; white space 102 | SP = %x20 ; space 103 | HTAB = %x09 ; horizontal tab 104 | ''' 105 | def is_LWS(b): 106 | return (b in b'\x20\x09\r\n') 107 | 108 | ''' 109 | header-value = *(TEXT-UTF8char / UTF8-CONT / LWS) 110 | ''' 111 | def is_header_value(b): 112 | return is_TEXT_UTF8char(b) or \ 113 | is_UTF8_CONT(b) or \ 114 | is_LWS(b) or \ 115 | (b in b'\x00\x07\x7F') # NUL BEL DEL - not sure if valid 116 | 117 | 118 | GENERATORS = { 119 | 'TOKEN_MAP': is_token, 120 | 'REQUEST_URI_MAP': is_request_uri, 121 | 'REASON_PHRASE_MAP': is_reason_phrase, 122 | 'HEADER_VALUE_MAP': is_header_value, 123 | } 124 | 125 | def main(args): 126 | for arg in args: 127 | func = GENERATORS[arg] 128 | generate_lookup_table(arg, func) 129 | 130 | main(sys.argv[1:]) 131 | -------------------------------------------------------------------------------- /src/lookup.rs: -------------------------------------------------------------------------------- 1 | 2 | macro_rules! byte_map { 3 | ($($flag:expr,)*) => ([ 4 | $($flag != 0,)* 5 | ]) 6 | } 7 | 8 | static TOKEN_MAP: [bool; 256] = byte_map![ 9 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11 | 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 12 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 13 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 15 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 17 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25 | ]; 26 | 27 | static REQUEST_URI_MAP: [bool; 256] = byte_map![ 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30 | 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 32 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 33 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 34 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 35 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44 | ]; 45 | 46 | static REASON_PHRASE_MAP: [bool; 256] = byte_map![ 47 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 48 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49 | 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 50 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 51 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 52 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 53 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 54 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 55 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 63 | ]; 64 | 65 | static HEADER_VALUE_MAP: [bool; 256] = byte_map![ 66 | 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 67 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 69 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 70 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 71 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 72 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 73 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 74 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 75 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 77 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 78 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 79 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 80 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 81 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 82 | ]; 83 | 84 | /// Determines if byte is a token char. 85 | #[inline] 86 | pub fn is_token(b: u8) -> bool { 87 | TOKEN_MAP[b as usize] 88 | } 89 | 90 | #[inline] 91 | pub fn is_request_uri(b: u8) -> bool { 92 | REQUEST_URI_MAP[b as usize] 93 | } 94 | 95 | #[inline] 96 | pub fn is_reason_phrase(b: u8) -> bool { 97 | REASON_PHRASE_MAP[b as usize] 98 | } 99 | 100 | /// Determines if byte is a header value char. 101 | #[inline] 102 | pub fn is_header_value(b: u8) -> bool { 103 | HEADER_VALUE_MAP[b as usize] 104 | } 105 | -------------------------------------------------------------------------------- /tests/requests.rs: -------------------------------------------------------------------------------- 1 | extern crate parsip; 2 | 3 | use parsip::{IResult, Request, Header, EMPTY_HEADER, SipVersion}; 4 | 5 | const NUM_OF_HEADERS: usize = 32; 6 | 7 | macro_rules! req { 8 | ($name:ident, $buf:expr, |$arg:ident| $body:expr) => ( 9 | req! {$name, $buf, |buf| IResult::Done(&buf[buf.len()..], buf.len()), |$arg| $body } 10 | ); 11 | ($name:ident, $buf:expr, |$res_arg:ident| $res_body:expr, |$arg:ident| $body:expr) => ( 12 | #[test] 13 | fn $name() { 14 | let mut headers = [EMPTY_HEADER; NUM_OF_HEADERS]; 15 | let mut req = Request::new(&mut headers[..]); 16 | let result = req.parse($buf.as_ref()); 17 | assert_eq!(result, res_closure($buf)); 18 | closure(req); 19 | 20 | fn res_closure($res_arg: &[u8]) -> IResult<&[u8], usize> { 21 | $res_body 22 | } 23 | 24 | fn closure($arg: Request) { 25 | $body 26 | } 27 | } 28 | ) 29 | } 30 | 31 | /// Helper for debugging 32 | fn print_headers(headers: &[Header]) { 33 | for header in headers.iter() { 34 | println!("Header {{ name: {:?}, value: {:?} }}", 35 | header.name, 36 | std::str::from_utf8(header.value).unwrap()); 37 | } 38 | } 39 | 40 | /// From [RFC 4475](https://tools.ietf.org/html/rfc4475#section-3.1.1.1): 41 | /// 3.1.1. Valid Messages 42 | /// 3.1.1.1. A Short Tortuous INVITE 43 | req! { 44 | test_request_short_tortuous_invite, 45 | include_bytes!("requests/wsinv.dat"), 46 | |buf| IResult::Done(&buf[857..], 857), 47 | |req| { 48 | assert_eq!(req.method.unwrap(), "INVITE"); 49 | assert_eq!(req.path.unwrap(), "sip:vivekg@chair-dnrc.example.com;unknownparam"); 50 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 51 | assert_eq!(req.headers.len(), 14); 52 | print_headers(req.headers); 53 | } 54 | } 55 | 56 | /// From [RFC 4475](https://tools.ietf.org/html/rfc4475#section-3.1.1.2): 57 | /// 3.1.1. Valid Messages 58 | /// 3.1.1.2. Wide Range of Valid Characters 59 | req! { 60 | test_request_wide_range_of_valid_characters, 61 | include_bytes!("requests/intmeth.dat"), 62 | |buf| IResult::Done(&buf[681..], 681), 63 | |req| { 64 | assert_eq!(req.method.unwrap(), "!interesting-Method0123456789_*+`.%indeed\'~"); 65 | assert_eq!(req.path.unwrap(), 66 | "sip:1_unusual.URI~(to-be!sure)&isn\'t+it$/crazy?,/;;*:&it+has=1,\ 67 | weird!*pas$wo~d_too.(doesn\'t-it)@example.com"); 68 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 69 | assert_eq!(req.headers.len(), 8); 70 | print_headers(req.headers); 71 | } 72 | } 73 | 74 | /// From [RFC 4475](https://tools.ietf.org/html/rfc4475#section-3.1.1.3): 75 | /// 3.1.1. Valid Messages 76 | /// 3.1.1.3. Valid Use of the % Escaping Mechanism 77 | req! { 78 | test_request_valid_use_of_the_percent_escaping_mechanism, 79 | include_bytes!("requests/esc01.dat"), 80 | |buf| IResult::Done(&buf[409..], 409), 81 | |req| { 82 | assert_eq!(req.method.unwrap(), "INVITE"); 83 | assert_eq!(req.path.unwrap(), "sip:sips%3Auser%40example.com@example.net"); 84 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 85 | assert_eq!(req.headers.len(), 9); 86 | print_headers(req.headers); 87 | } 88 | } 89 | 90 | /// From [RFC 4475](https://tools.ietf.org/html/rfc4475#section-3.1.1.4): 91 | /// 3.1.1. Valid Messages 92 | /// 3.1.1.4. Escaped Nulls in URIs 93 | req! { 94 | test_request_escaped_nulls_in_uris, 95 | include_bytes!("requests/escnull.dat"), 96 | |buf| IResult::Done(&buf[365..], 365), 97 | |req| { 98 | assert_eq!(req.method.unwrap(), "REGISTER"); 99 | assert_eq!(req.path.unwrap(), "sip:example.com"); 100 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 101 | assert_eq!(req.headers.len(), 9); 102 | print_headers(req.headers); 103 | } 104 | } 105 | 106 | /// From [RFC 4475](https://tools.ietf.org/html/rfc4475#section-3.1.1.5): 107 | /// 3.1.1. Valid Messages 108 | /// 3.1.1.5. Use of % When It Is Not an Escape 109 | req! { 110 | test_request_use_of_percent_when_it_is_not_an_escape, 111 | include_bytes!("requests/esc02.dat"), 112 | |buf| IResult::Done(&buf[445..], 445), 113 | |req| { 114 | assert_eq!(req.method.unwrap(), "RE%47IST%45R"); 115 | assert_eq!(req.path.unwrap(), "sip:registrar.example.com"); 116 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 117 | assert_eq!(req.headers.len(), 10); 118 | print_headers(req.headers); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/sip.rs: -------------------------------------------------------------------------------- 1 | use nom::{digit, is_space, line_ending, crlf, rest}; 2 | use std::{str, slice}; 3 | use lookup::{is_token, is_request_uri, is_reason_phrase, is_header_value}; 4 | 5 | /// A Result of any parsing action. 6 | /// 7 | /// If the input is invalid, an `IResult::Error` will be returned. 8 | /// Note that incomplete data is not considered invalid, 9 | /// and so will not return an error, but rather a `IResult::Incomplete(_)`. 10 | pub use nom::{IResult, Err, ErrorKind, Needed}; 11 | 12 | #[inline] 13 | fn shrink(slice: &mut &mut [T], len: usize) { 14 | debug_assert!(slice.len() >= len); 15 | let ptr = slice.as_mut_ptr(); 16 | *slice = unsafe { slice::from_raw_parts_mut(ptr, len) }; 17 | } 18 | 19 | /// An error in parsing. 20 | /// TODO: for now this is unused; use this custom error type 21 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 22 | pub enum Error { 23 | /// Invalid byte in header name. 24 | HeaderName, 25 | /// Invalid byte in header value. 26 | HeaderValue, 27 | /// Invalid byte in new line. 28 | NewLine, 29 | /// Invalid byte in Response status. 30 | Status, 31 | /// Invalid byte where token is required. 32 | Token, 33 | /// Parsed more headers than provided buffer can contain. 34 | TooManyHeaders, 35 | /// Invalid byte in SIP version. 36 | Version, 37 | } 38 | 39 | /// A parsed Request. 40 | /// 41 | /// The optional values will be `None` if a parse was not complete, and did not 42 | /// parse the associated property. This allows you to inspect the parts that 43 | /// could be parsed, before reading more, in case you wish to exit early. 44 | /// 45 | /// # Example 46 | /// 47 | /// ``` 48 | /// let buf = b"INVITE sip:callee@domain.com SIP/2.0\r\nHost:"; 49 | /// let mut headers = [parsip::EMPTY_HEADER; 16]; 50 | /// let mut req = parsip::Request::new(&mut headers); 51 | /// let res = req.parse(buf); 52 | /// if let parsip::IResult::Incomplete(_) = res { 53 | /// match req.path { 54 | /// Some(ref path) => { 55 | /// // check router for path. 56 | /// // is domain.com unreachable? we could stop parsing 57 | /// }, 58 | /// None => { 59 | /// // must read more and parse again 60 | /// } 61 | /// } 62 | /// } 63 | /// ``` 64 | #[derive(Debug, PartialEq)] 65 | pub struct Request<'headers, 'buf: 'headers> { 66 | /// The request method, such as `INVITE`. 67 | pub method: Option<&'buf str>, 68 | /// The request path, such as `sip:callee@domain.com`. 69 | pub path: Option<&'buf str>, 70 | /// The request version, such as `SIP/2.0`. 71 | pub version: Option, 72 | /// The request headers. 73 | pub headers: &'headers mut [Header<'buf>], 74 | } 75 | 76 | impl<'h, 'b> Request<'h, 'b> { 77 | /// Creates a new Request, using a slice of headers you allocate. 78 | #[inline] 79 | pub fn new(headers: &'h mut [Header<'b>]) -> Request<'h, 'b> { 80 | Request { 81 | method: None, 82 | path: None, 83 | version: None, 84 | headers: headers, 85 | } 86 | } 87 | 88 | /// > ```notrust 89 | /// > Request-Line = Method SP Request-URI SP SIP-Version CRLF 90 | /// > ``` 91 | // TODO: extract parse_request_line method when figure out how 92 | pub fn parse(&mut self, buf: &'b [u8]) -> IResult<&'b [u8], usize> { 93 | do_parse!(buf, 94 | begin: rest_len >> 95 | skip_empty_lines >> 96 | map!(parse_method, |method| self.method = Some(method)) >> char!(' ') >> 97 | map!(parse_request_uri, |path| self.path = Some(path)) >> char!(' ') >> 98 | map!(parse_version, |version| self.version = Some(version)) >> crlf >> 99 | headers_len: map!(call!(parse_headers, self.headers), |headers| headers.len()) >> 100 | crlf >> 101 | end: rest_len >> 102 | ({ 103 | shrink(&mut self.headers, headers_len); 104 | begin - end 105 | }) 106 | ) 107 | } 108 | } 109 | 110 | /// Helper that results in number of remaining bytes 111 | named!(#[inline], rest_len, map!(peek!(rest), |buf| buf.len())); 112 | 113 | /// Helper that skips all `\r\n` or `\n` bytes 114 | named!(#[inline], skip_empty_lines<()>, 115 | fold_many0!(line_ending, (), |_, _| ()) 116 | ); 117 | 118 | /// A parsed Response. 119 | /// 120 | /// See `Request` docs for explanation of optional values. 121 | #[derive(Debug, PartialEq)] 122 | pub struct Response<'headers, 'buf: 'headers> { 123 | /// The response version, such as `SIP/2.0`. 124 | pub version: Option, 125 | /// The response code, such as `200`. 126 | pub code: Option, 127 | /// The response reason-phrase, such as `OK`. 128 | pub reason: Option<&'buf str>, 129 | /// The response headers. 130 | pub headers: &'headers mut [Header<'buf>], 131 | } 132 | 133 | impl<'h, 'b> Response<'h, 'b> { 134 | /// Creates a new `Response` using a slice of `Header`s you have allocated. 135 | #[inline] 136 | pub fn new(headers: &'h mut [Header<'b>]) -> Response<'h, 'b> { 137 | Response { 138 | version: None, 139 | code: None, 140 | reason: None, 141 | headers: headers, 142 | } 143 | } 144 | 145 | /// Try to parse a buffer of bytes into this `Response`. 146 | /// 147 | /// > ```notrust 148 | /// > Status-Line = SIP-Version SP Status-Code SP Reason-Phrase CRLF 149 | /// > ``` 150 | // TODO: extract parse_status_line method when figure out how 151 | pub fn parse(&mut self, buf: &'b [u8]) -> IResult<&'b [u8], usize> { 152 | do_parse!(buf, 153 | begin: rest_len >> 154 | skip_empty_lines >> 155 | map!(parse_version, |version| self.version = Some(version)) >> char!(' ') >> 156 | map!(parse_code, |code| self.code = Some(code)) >> char!(' ') >> 157 | map!(parse_reason, |reason| self.reason = Some(reason)) >> crlf >> 158 | headers_len: map!(call!(parse_headers, self.headers), |headers| headers.len()) >> 159 | crlf >> 160 | end: rest_len >> 161 | ({ 162 | shrink(&mut self.headers, headers_len); 163 | begin - end 164 | }) 165 | ) 166 | } 167 | } 168 | 169 | /// Represents a parsed header. 170 | #[derive(Copy, Clone, PartialEq, Debug)] 171 | pub struct Header<'a> { 172 | /// The name portion of a header. 173 | /// 174 | /// A header name must be valid US-ASCII, so it's safe to store as a `&str`. 175 | pub name: &'a str, 176 | /// The value portion of a header. 177 | /// 178 | /// While headers **should** be US-ASCII, the specification allows for 179 | /// values that may not be, and so the value is stored as bytes. 180 | pub value: &'a [u8], 181 | } 182 | 183 | /// An empty header, useful for constructing a `Header` array to pass in for 184 | /// parsing. 185 | /// 186 | /// # Example 187 | /// 188 | /// ``` 189 | ///# #![allow(unused_variables)] 190 | /// let headers = [parsip::EMPTY_HEADER; 64]; 191 | /// ``` 192 | pub const EMPTY_HEADER: Header<'static> = Header { 193 | name: "", 194 | value: b"", 195 | }; 196 | 197 | /// SIP-Version 198 | /// ex. `SIP/2.0 -> SipVersion(2, 0)` 199 | #[derive(Copy, Clone, PartialEq, Debug)] 200 | pub struct SipVersion(pub u8, pub u8); 201 | 202 | /// Get one digit from input and return it as `u8` (ie. `b'7'` becomes `7`) 203 | named!(#[inline], single_digit<&[u8], u8>, 204 | map!( 205 | flat_map!(take!(1), digit), 206 | |a| a[0] - b'0' 207 | ) 208 | ); 209 | 210 | /// Eats token bytes 211 | named!(#[inline], parse_token<&[u8], &str>, 212 | map_res!(take_while1!(is_token), str::from_utf8) 213 | ); 214 | 215 | /// > ```notrust 216 | /// > Method = INVITEm / ACKm / OPTIONSm / BYEm 217 | /// > / CANCELm / REGISTERm 218 | /// > / extension-method 219 | /// > extension-method = token 220 | /// > ``` 221 | named!(#[inline], parse_method<&[u8], &str>, 222 | call!(parse_token) 223 | ); 224 | 225 | /// As parsing uri is a bit complicated, it should be properly 226 | /// parsed in higher layers of parsing. 227 | /// 228 | /// > ```notrust 229 | /// > Request-URI = SIP-URI / SIPS-URI / absoluteURI 230 | /// > absoluteURI = scheme ":" ( hier-part / opaque-part ) 231 | /// > hier-part = ( net-path / abs-path ) [ "?" query ] 232 | /// > net-path = "//" authority [ abs-path ] 233 | /// > abs-path = "/" path-segments 234 | /// > SIP-URI = "sip:" [ userinfo ] hostport 235 | /// > uri-parameters [ headers ] 236 | /// > SIPS-URI = "sips:" [ userinfo ] hostport 237 | /// > uri-parameters [ headers ] 238 | /// > userinfo = ( user / telephone-subscriber ) [ ":" password ] "@" 239 | /// > user = 1*( unreserved / escaped / user-unreserved ) 240 | /// > user-unreserved = "&" / "=" / "+" / "$" / "," / ";" / "?" / "/" 241 | /// > password = *( unreserved / escaped / 242 | /// > "&" / "=" / "+" / "$" / "," ) 243 | /// > hostport = host [ ":" port ] 244 | /// > host = hostname / IPv4address / IPv6reference 245 | /// > hostname = *( domainlabel "." ) toplabel [ "." ] 246 | /// > domainlabel = alphanum 247 | /// > / alphanum *( alphanum / "-" ) alphanum 248 | /// > toplabel = ALPHA / ALPHA *( alphanum / "-" ) alphanum 249 | /// > ``` 250 | named!(#[inline], parse_request_uri<&[u8], &str>, 251 | map_res!(take_while1!(is_request_uri), str::from_utf8) 252 | ); 253 | 254 | /// From [RFC 3261](https://tools.ietf.org/html/rfc3261#section-7.1): 255 | /// 256 | /// The SIP-Version string is case-insensitive, 257 | /// but implementations MUST send upper-case. 258 | /// 259 | /// > ```notrust 260 | /// > SIP-Version = "SIP" "/" 1*DIGIT "." 1*DIGIT 261 | /// > ``` 262 | named!(#[inline], parse_version, 263 | do_parse!( 264 | tag_no_case!("SIP/") >> 265 | x: single_digit >> 266 | char!('.') >> 267 | y: single_digit >> 268 | ( SipVersion(x, y) ) 269 | ) 270 | ); 271 | 272 | /// From [RFC 3261](https://tools.ietf.org/html/rfc3261): 273 | /// 274 | /// > ```notrust 275 | /// > Reason-Phrase = *(reserved / unreserved / escaped 276 | /// > / UTF8-NONASCII / UTF8-CONT / SP / HTAB) 277 | /// > ``` 278 | named!(#[inline], parse_reason<&[u8], &str>, 279 | map_res!(take_while!(is_reason_phrase), str::from_utf8) 280 | ); 281 | 282 | // From [RFC 3261](https://tools.ietf.org/html/rfc3261): 283 | /// 284 | /// > ```notrust 285 | /// > Status-Code = Informational 286 | /// > / Redirection 287 | /// > / Success 288 | /// > / Client-Error 289 | /// > / Server-Error 290 | /// > / Global-Failure 291 | /// > / extension-code 292 | /// > ``` 293 | named!(#[inline], parse_code<&[u8], u16>, 294 | map!( 295 | flat_map!(take!(3), digit), 296 | |arr| (arr[0] - b'0') as u16 * 100 + (arr[1] - b'0') as u16 * 10 + 297 | (arr[2] - b'0') as u16 298 | ) 299 | ); 300 | 301 | /// > ```notrust 302 | /// > header-name = token 303 | /// > ``` 304 | named!(#[inline], header_name<&[u8], &str>, 305 | map_res!( 306 | take_while!(is_token), 307 | str::from_utf8 308 | ) 309 | ); 310 | 311 | /// From [RFC 3261](https://tools.ietf.org/html/rfc3261#section-7.3.1): 312 | /// 313 | /// Header fields can be extended over multiple lines by preceding each 314 | /// extra line with at least one SP or horizontal tab (HT). 315 | /// 316 | /// Header value may be empty! 317 | /// 318 | fn header_value(buf: &[u8]) -> IResult<&[u8], &[u8]> { 319 | use self::IResult::*; 320 | 321 | let mut end_pos = 0; 322 | let mut idx = 0; 323 | while idx < buf.len() { 324 | match buf[idx] { 325 | b'\n' => { 326 | idx += 1; 327 | if idx >= buf.len() { 328 | return Incomplete(Needed::Size(1)); 329 | } 330 | match buf[idx] { 331 | b' ' | b'\t' => { 332 | idx += 1; 333 | continue; 334 | } 335 | _ => { 336 | return Done(&buf[end_pos..], &buf[..end_pos]); 337 | } 338 | } 339 | } 340 | b' ' | b'\t' | b'\r' => {} 341 | b => { 342 | if !is_header_value(b) { 343 | return Error(error_position!(ErrorKind::Custom(b as u32), buf)); 344 | } 345 | end_pos = idx + 1; 346 | } 347 | } 348 | idx += 1; 349 | } 350 | Done(&b""[..], buf) 351 | } 352 | 353 | /// > ```notrust 354 | /// > HCOLON = *( SP / HTAB ) ":" SWS 355 | /// > ``` 356 | named!(hcolon, delimited!( 357 | take_while!(is_space), 358 | char!(':'), 359 | take_while!(is_space) 360 | )); 361 | 362 | /// > ```notrust 363 | /// > header = "header-name" HCOLON header-value *(COMMA header-value) 364 | /// > ``` 365 | named!(message_header
, do_parse!( 366 | n: header_name >> 367 | hcolon >> 368 | v: header_value >> 369 | crlf >> 370 | (Header{ name: n, value: v }) 371 | )); 372 | 373 | /// Parse a buffer of bytes as headers. 374 | /// 375 | /// The return value, if complete and successful, includes the index of the 376 | /// buffer that parsing stopped at, and a sliced reference to the parsed 377 | /// headers. The length of the slice will be equal to the number of properly 378 | /// parsed headers. 379 | /// 380 | /// # Example 381 | /// 382 | /// ``` 383 | /// let buf = b"Host: foo.bar\r\nAccept: */*\r\n\r\n"; 384 | /// let mut headers = [parsip::EMPTY_HEADER; 4]; 385 | /// assert_eq!(parsip::parse_headers(buf, &mut headers), 386 | /// parsip::IResult::Done(&buf[28..], &[ 387 | /// parsip::Header { name: "Host", value: b"foo.bar" }, 388 | /// parsip::Header { name: "Accept", value: b"*/*" } 389 | /// ][..])); 390 | /// ``` 391 | pub fn parse_headers<'b: 'h, 'h>(mut input: &'b [u8], 392 | mut headers: &'h mut [Header<'b>]) 393 | -> IResult<&'b [u8], &'h [Header<'b>]> { 394 | use self::IResult::*; 395 | let mut i = 0; 396 | while i < headers.len() { 397 | match crlf(input) { 398 | Done(_, _) => break, 399 | Error(_) => {} 400 | Incomplete(e) => return Incomplete(e), 401 | }; 402 | let (rest, header) = try_parse!(input, message_header); 403 | headers[i] = header; 404 | input = rest; 405 | i += 1; 406 | } 407 | 408 | shrink(&mut headers, i); 409 | Done(input, headers) 410 | } 411 | 412 | 413 | #[cfg(test)] 414 | mod tests { 415 | use super::{IResult, ErrorKind, Needed}; 416 | use super::{Request, Response, EMPTY_HEADER, SipVersion}; 417 | 418 | const NUM_OF_HEADERS: usize = 4; 419 | 420 | macro_rules! req { 421 | ($name:ident, $buf:expr, |$arg:ident| $body:expr) => ( 422 | req! {$name, $buf, 423 | |buf| IResult::Done(&buf[buf.len()..], buf.len()), 424 | |$arg| $body } 425 | ); 426 | ($name:ident, $buf:expr, 427 | |$res_arg:ident| $res_body:expr, 428 | |$arg:ident| $body:expr) => ( 429 | #[test] 430 | fn $name() { 431 | let mut headers = [EMPTY_HEADER; NUM_OF_HEADERS]; 432 | let mut req = Request::new(&mut headers); 433 | let result = req.parse($buf.as_ref()); 434 | assert_eq!(result, res_closure($buf)); 435 | closure(req); 436 | 437 | fn res_closure($res_arg: &[u8]) -> IResult<&[u8], usize> { 438 | $res_body 439 | } 440 | 441 | fn closure($arg: Request) { 442 | $body 443 | } 444 | } 445 | ) 446 | } 447 | 448 | #[test] 449 | fn test_header_value_empty() { 450 | let buf = b"\r\nAccept: */*\r\n\r\n"; 451 | assert_eq!(super::header_value(buf), 452 | IResult::Done(&buf[0..], &buf[..0])); 453 | } 454 | 455 | req! { 456 | test_request_simple, 457 | b"INVITE sip:callee@domain.com SIP/2.0\r\n\r\n", 458 | |req| { 459 | assert_eq!(req.method.unwrap(), "INVITE"); 460 | assert_eq!(req.path.unwrap(), "sip:callee@domain.com"); 461 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 462 | assert_eq!(req.headers.len(), 0); 463 | } 464 | } 465 | 466 | req! { 467 | test_request_headers, 468 | b"INVITE sip:callee@domain.com SIP/2.0\r\n\ 469 | Host: foo.com\r\n\ 470 | To: \r\n\ 471 | \r\n", 472 | |req| { 473 | assert_eq!(req.method.unwrap(), "INVITE"); 474 | assert_eq!(req.path.unwrap(), "sip:callee@domain.com"); 475 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 476 | assert_eq!(req.headers.len(), 2); 477 | assert_eq!(req.headers[0].name, "Host"); 478 | assert_eq!(req.headers[0].value, b"foo.com"); 479 | assert_eq!(req.headers[1].name, "To"); 480 | assert_eq!(req.headers[1].value, b""); 481 | } 482 | } 483 | 484 | req! { 485 | test_request_headers_max, 486 | b"INVITE sip:callee@domain.com SIP/2.0\r\n\ 487 | A: A\r\n\ 488 | B: B\r\n\ 489 | C: C\r\n\ 490 | D: D\r\n\ 491 | \r\n", 492 | |req| { 493 | assert_eq!(req.headers.len(), NUM_OF_HEADERS); 494 | } 495 | } 496 | 497 | req! { 498 | test_request_multibyte, 499 | b"INVITE sip:callee@domain.com SIP/2.0\r\nHost: foo.com\r\n\ 500 | User-Agent: \xe3\x81\xb2\xe3/1.0\r\n\r\n", 501 | |req| { 502 | assert_eq!(req.method.unwrap(), "INVITE"); 503 | assert_eq!(req.path.unwrap(), "sip:callee@domain.com"); 504 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 505 | assert_eq!(req.headers[0].name, "Host"); 506 | assert_eq!(req.headers[0].value, b"foo.com"); 507 | assert_eq!(req.headers[1].name, "User-Agent"); 508 | assert_eq!(req.headers[1].value, b"\xe3\x81\xb2\xe3/1.0"); 509 | } 510 | } 511 | 512 | req! { 513 | test_request_partial, 514 | b"INVITE sip:callee@domain.com SIP/2.0\r\n\r", 515 | |_buf| IResult::Incomplete(Needed::Size(40)), 516 | |_req| {} 517 | } 518 | 519 | req! { 520 | test_request_newlines, 521 | b"INVITE sip:callee@domain.com SIP/2.0\nHost: foo.bar\n\n", 522 | |_buf| IResult::Error(error_position!(ErrorKind::CrLf, &_buf[36..])), 523 | |_req| {} 524 | } 525 | 526 | req! { 527 | test_request_empty_lines_prefix, 528 | b"\r\n\r\nINVITE sip:callee@domain.com SIP/2.0\r\n\r\n", 529 | |req| { 530 | assert_eq!(req.method.unwrap(), "INVITE"); 531 | assert_eq!(req.path.unwrap(), "sip:callee@domain.com"); 532 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 533 | assert_eq!(req.headers.len(), 0); 534 | } 535 | } 536 | 537 | req! { 538 | test_request_empty_lines_prefix_lf_only, 539 | b"\n\nINVITE sip:callee@domain.com SIP/2.0\r\n\r\n", 540 | |req| { 541 | assert_eq!(req.method.unwrap(), "INVITE"); 542 | assert_eq!(req.path.unwrap(), "sip:callee@domain.com"); 543 | assert_eq!(req.version.unwrap(), SipVersion(2,0)); 544 | } 545 | } 546 | 547 | req! { 548 | test_request_with_invalid_token_delimiter, 549 | b"GET\n/ SIP/2.0\r\nHost: foo.bar\r\n\r\n", 550 | |_buf| IResult::Error(error_position!(ErrorKind::Char, &_buf[3..])), 551 | |_req| {} 552 | } 553 | 554 | req! { 555 | test_request_headers_with_continuation, 556 | b"INVITE sip:callee@domain.com SIP/2.0\r\n\ 557 | NewFangledHeader: newfangled value\r\n continued newfangled value\r\n\ 558 | \r\n", 559 | |req| { 560 | assert_eq!(req.headers.len(), 1); 561 | assert_eq!(req.headers[0].name, "NewFangledHeader"); 562 | assert_eq!(req.headers[0].value, 563 | &b"newfangled value\r\n continued newfangled value"[..]); 564 | } 565 | } 566 | 567 | macro_rules! res { 568 | ($name:ident, $buf:expr, |$arg:ident| $body:expr) => ( 569 | res! {$name, $buf, 570 | |buf| IResult::Done(&buf[buf.len()..], buf.len()), 571 | |$arg| $body } 572 | ); 573 | ($name:ident, $buf:expr, 574 | |$res_arg:ident| $res_body:expr, 575 | |$arg:ident| $body:expr) => ( 576 | #[test] 577 | fn $name() { 578 | let mut headers = [EMPTY_HEADER; NUM_OF_HEADERS]; 579 | let mut res = Response::new(&mut headers); 580 | let result = res.parse($buf.as_ref()); 581 | assert_eq!(result, res_closure($buf)); 582 | closure(res); 583 | 584 | fn res_closure($res_arg: &[u8]) -> IResult<&[u8], usize> { 585 | $res_body 586 | } 587 | 588 | fn closure($arg: Response) { 589 | $body 590 | } 591 | } 592 | ) 593 | } 594 | 595 | res! { 596 | test_response_simple, 597 | b"SIP/2.0 200 OK\r\n\r\n", 598 | |res| { 599 | assert_eq!(res.version.unwrap(), SipVersion(2,0)); 600 | assert_eq!(res.code.unwrap(), 200); 601 | assert_eq!(res.reason.unwrap(), "OK"); 602 | } 603 | } 604 | 605 | res! { 606 | test_response_newlines, 607 | b"SIP/2.0 403 Forbidden\nServer: foo.bar\n\n", 608 | |_buf| IResult::Error(error_position!(ErrorKind::CrLf, &_buf[21..])), 609 | |_res| {} 610 | } 611 | 612 | res! { 613 | test_response_reason_missing, 614 | b"SIP/2.0 200 \r\n\r\n", 615 | |res| { 616 | assert_eq!(res.version.unwrap(), SipVersion(2,0)); 617 | assert_eq!(res.code.unwrap(), 200); 618 | assert_eq!(res.reason.unwrap(), ""); 619 | } 620 | } 621 | 622 | res! { 623 | test_response_reason_missing_no_space, 624 | b"SIP/2.0 200\r\n\r\n", 625 | |_buf| IResult::Error(error_position!(ErrorKind::Char, &_buf[11..])), 626 | |res| { 627 | assert_eq!(res.version.unwrap(), SipVersion(2,0)); 628 | assert_eq!(res.code.unwrap(), 200); 629 | } 630 | } 631 | 632 | res! { 633 | test_response_reason_with_space_and_tab, 634 | b"SIP/2.0 101 Switching Protocols\t\r\n\r\n", 635 | |res| { 636 | assert_eq!(res.version.unwrap(), SipVersion(2,0)); 637 | assert_eq!(res.code.unwrap(), 101); 638 | assert_eq!(res.reason.unwrap(), "Switching Protocols\t"); 639 | } 640 | } 641 | 642 | static RESPONSE_REASON_WITH_OBS_TEXT_BYTE: &'static [u8] = b"SIP/2.0 200 X\xFFZ\r\n\r\n"; 643 | res! { 644 | test_response_reason_with_obsolete_text_byte, 645 | RESPONSE_REASON_WITH_OBS_TEXT_BYTE, 646 | |_buf| IResult::Error(error_position!(ErrorKind::MapRes, &_buf[12..])), 647 | |_res| {} 648 | } 649 | 650 | res! { 651 | test_response_reason_with_nul_byte, 652 | b"SIP/2.0 200 \x00\r\n\r\n", 653 | |_buf| IResult::Error(error_position!(ErrorKind::CrLf, &_buf[12..])), 654 | |_res| {} 655 | } 656 | 657 | res! { 658 | test_response_version_missing_space, 659 | b"SIP/2.0", 660 | |_buf| IResult::Incomplete(Needed::Size(8)), 661 | |_res| {} 662 | } 663 | 664 | res! { 665 | test_response_code_missing_space, 666 | b"SIP/2.0 200", 667 | |_buf| IResult::Incomplete(Needed::Size(12)), 668 | |_res| {} 669 | } 670 | 671 | res! { 672 | test_response_empty_lines_prefix_lf_only, 673 | b"\n\nSIP/2.0 200 OK\n\n", 674 | |_buf| IResult::Error(error_position!(ErrorKind::CrLf, &_buf[16..])), 675 | |_res| {} 676 | } 677 | } 678 | --------------------------------------------------------------------------------