minihttpse 0.1.6

a mini http response parser lib in rust
Documentation
//!minihttpse is a http response parser for rust,is simple and easy
//! # Examples
//! ```
//! use minihttpse::Response;
//!
//! let s = "HTTP/1.1 200 OK\r\n\
//!         Content-Length: 18\r\n\
//!         Server: GWS/2.0\r\n\
//!         Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\
//!         Content-Type: text/html\r\n\
//!         Cache-control: private\r\n\
//!         Set-Cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S=SMCc_HRPCQiqyX9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com\r\n\
//!         Connection: keep-alive\r\n\
//!         \r\n\
//!         <html>hello</html>";
//! let res = Response::new(s.as_bytes().to_owned()).unwrap();
//! assert_eq!(res.reason(),"OK");
//! assert_eq!(res.status_code(),200);
//! assert_eq!(res.headers().get("Server"),Some(&"GWS/2.0".to_owned()));
//! println!("body {}",res.text());
//! ```

#![doc(html_root_url = "https://docs.rs/miniurl")]

use std::collections::HashMap;
use std::usize;
use std::str;


///http response parse error type
#[derive(Debug,Clone)]
pub enum HttpError{
    Parse(&'static str)
}

const CR: u8 = b'\r';
const LF: u8 = b'\n';

///http response object
#[derive(Debug,Clone)]
pub struct Response{
    status_code:usize,
    reason:String,
    headers:HashMap<String,String>,
    body:Vec<u8>
}

impl Response{
    ///parse http response to Response object.
    /// # Examples
    /// ```
    /// use minihttpse::Response;
    ///
    /// let s = "HTTP/1.1 200 OK\r\n\
    ///         Content-Length: 18\r\n\
    ///         Server: GWS/2.0\r\n\
    ///         Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\
    ///         Content-Type: text/html\r\n\
    ///         Cache-control: private\r\n\
    ///         Set-Cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S=SMCc_HRPCQiqyX9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com\r\n\
    ///         Connection: keep-alive\r\n\
    ///         \r\n\
    ///         <html>hello</html>";
    /// let res = Response::new(s.as_bytes().to_owned()).unwrap();
    /// assert_eq!(res.reason(),"OK");
    /// assert_eq!(res.status_code(),200);
    /// assert_eq!(res.headers().get("Server"),Some(&"GWS/2.0".to_owned()));
    /// println!("body {}",res.text());
    /// ```
    ///
    /// ##chunked response example
    /// ```
    /// use minihttpse::Response;
    ///
    /// let s = "HTTP/1.1 200 OK\r\n\
    ///          Server: GWS/2.0\r\n\
    ///          Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\
    ///          Content-Type: text/html\r\n\
    ///          Connection: keep-alive\r\n\
    ///          Transfer-Encoding: chunked\r\n\
    ///          \r\n\
    ///          b\r\n\
    ///          01234567890\r\n\
    ///          6\r\n\
    ///          123456\r\n\
    ///          3\r\n\
    ///          abc\r\n\
    ///          0\r\n\
    ///          \r\n";
    /// let res = Response::new(s.as_bytes().to_owned()).unwrap();
    /// assert_eq!(res.reason(),"OK");
    /// assert_eq!(res.status_code(),200);
    /// assert_eq!(res.headers().get("Server"),Some(&"GWS/2.0".to_owned()));
    /// println!("body {}",res.text());
    /// ```
    ///

    pub fn new(res:Vec<u8>)->Result<Response,HttpError>{
        let mut pos:usize = 0;
        for i in 0..res.len()-1{
            if res[i] == CR && res[i+1] ==LF &&res[i+2] ==CR && res[i+3] ==LF{
                pos = i+3;
                break;
            }
        }
        if pos == 0{
            return Err(HttpError::Parse("not http response"))
        }

        let (h,b):(&[u8],&[u8]) = res.split_at(pos);
        let header = match String::from_utf8(h.to_vec()){
            Ok(h) =>h,
            Err(_) =>return Err(HttpError::Parse("response header error"))
        };

        let body = b[1..].to_owned();

        let mut header_vec:Vec<&str> = header.split("\r\n").collect();
        let first_line = header_vec[0].to_owned();
        let first_line_vec:Vec<&str> = first_line.splitn(3," ").collect();
        let status_code:usize = match first_line_vec[1].parse(){
            Ok(s) =>s,
            Err(_) => return Err(HttpError::Parse("parse status code error"))
        };
        let reason = first_line_vec[2].to_owned();

        header_vec.remove(0);
        let len = header_vec.len();
        header_vec.remove(len - 1);

        let mut headers:HashMap<String,String> = HashMap::new();
        for i in header_vec{
            let item = i.to_owned();
            let item_vec:Vec<&str> = item.splitn(2,": ").collect();
            headers.insert(item_vec[0].to_owned(),item_vec[1].to_owned());
        }

        let b:Vec<u8> = match headers.get("Transfer-Encoding"){
            Some(t) =>{
                if t == &"chunked".to_string(){
                    let b = match Response::parse_chunk(body){
                        Ok(b) => b,
                        Err(_) => return  Err(HttpError::Parse("parse chunk error"))
                    };
                    b
                }else {
                    body
                }
            },
            None => body
        };
        Ok(Response{
            status_code:status_code,
            reason:reason,
            headers:headers,
            body:b
        })

    }

    //parse chunked body
     fn parse_chunk(body:Vec<u8>)->Result<Vec<u8>,HttpError>{
        let mut buf:Vec<u8> = Vec::new();
        let mut count:usize = 0;

        loop {
            let mut pos:usize = 0;
            for i in count..body.len()-1{
                if body[i] == CR && body[i+1] ==LF{
                    pos =i;
                    break
                }
            }
            if pos == 0{
                return Err(HttpError::Parse("chunked response error"))
            }

            let size_s = match str::from_utf8(&body[count..pos]){
                Ok(s) =>s,
                Err(_) => return Err(HttpError::Parse("get chunk size error"))
            };
            count = count + (pos - count) +2;
            let size:usize = match usize::from_str_radix(size_s, 16){
                Ok(s)=>s,
                Err(_)=> return Err(HttpError::Parse("parse chunk size error"))
            };
            if size == 0 && count+2 == body.len(){
                return Ok(buf);
            }
            buf.extend_from_slice(&body[pos+2..pos+2+size]);
            count = count + size +2;
        }

    }

    ///return status code(eg:200)
    pub fn status_code(&self)->usize{
        self.status_code
    }

    ///return response body string
    pub fn text(&self)->String{
        let body = self.body.clone();
        let text = String::from_utf8(body).unwrap();
        text
    }

    ///return response raw body
    pub fn content(&self)->Vec<u8>{
        self.body.clone()
    }

    ///return response headers hashmap
    pub fn headers(&self)->HashMap<String,String>{
        self.headers.clone()
    }

    ///return response reason
    pub fn reason(&self)->String{
        self.reason.clone()
    }
}



#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_check_parse() {
        let s = "HTTP/1.1 200 OK\r\n\
                        Content-Length: 18\r\n\
                        Server: GWS/2.0\r\n\
                        Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\
                        Content-Type: text/html\r\n\
                        Cache-control: private\r\n\
                        Set-Cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S=SMCc_HRPCQiqyX9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com\r\n\
                        Connection: keep-alive\r\n\
                        \r\n\
                        <html>hello</html>";
        let res = Response::new(s.as_bytes().to_owned()).unwrap();
        assert_eq!(res.reason(),"OK");
        assert_eq!(res.status_code(),200);
        assert_eq!(res.headers().get("Server"),Some(&"GWS/2.0".to_owned()));
        println!("body {}",res.text());

    }

    #[test]
    fn test_check_chunk_parse() {
        let s = "HTTP/1.1 200 OK\r\n\
                        Server: GWS/2.0\r\n\
                        Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\
                        Content-Type: text/html\r\n\
                        Connection: keep-alive\r\n\
                        Transfer-Encoding: chunked\r\n\
                        \r\n\
                        b\r\n\
                        01234567890\r\n\
                        6\r\n\
                        123456\r\n\
                        3\r\n\
                        abc\r\n\
                        0\r\n\
                        \r\n";
        let res = Response::new(s.as_bytes().to_owned()).unwrap();
        assert_eq!(res.reason(),"OK");
        assert_eq!(res.status_code(),200);
        assert_eq!(res.headers().get("Server"),Some(&"GWS/2.0".to_owned()));
        println!("body {}",res.text());

    }
}