#![doc(html_root_url = "https://docs.rs/miniurl")]
use std::collections::HashMap;
use std::usize;
use std::str;
#[derive(Debug,Clone)]
pub enum HttpError{
Parse(&'static str)
}
const CR: u8 = b'\r';
const LF: u8 = b'\n';
#[derive(Debug,Clone)]
pub struct Response{
status_code:usize,
reason:String,
headers:HashMap<String,String>,
body:Vec<u8>
}
impl Response{
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
})
}
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;
}
}
pub fn status_code(&self)->usize{
self.status_code
}
pub fn text(&self)->String{
let body = self.body.clone();
let text = String::from_utf8(body).unwrap();
text
}
pub fn content(&self)->Vec<u8>{
self.body.clone()
}
pub fn headers(&self)->HashMap<String,String>{
self.headers.clone()
}
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());
}
}