httpal 0.1.0

Work in progress: HTTP client for Rust
Documentation
use std::io::Read;
use std::collections::HashMap;
use std::mem;

use http::{Header, Error};

enum State {
    HeaderFieldFirstByte,
    HeaderField,
    HeaderFieldParsed,
    HeaderValueFirstByte,
    HeaderValue,
    HeaderValueAlmostParsed,
    HeadersAlmostParsed,
    HeadersParsed,
}

pub fn parse(reader: &mut Read) -> Result<HashMap<Header, String>, Error> {
    let mut bytes = reader.bytes().peekable();
    let mut state = State::HeaderFieldFirstByte;

    let mut headers: HashMap<Header, String> = HashMap::new();
    let mut current_header_field = String::new();
    let mut current_header_value = String::new();

    loop {
        match state {
            State::HeaderFieldFirstByte => {
                match bytes.next().unwrap() {
                    Ok(b'\r') => { state = State::HeadersAlmostParsed },
                    Ok(b' ') | Ok(b'\n') | Ok(b':') | Err(_) => return Err(Error::InvalidHeaderField),
                    Ok(byte) => {
                        current_header_field.push(byte as char);
                        state = State::HeaderField;
                    },
                }
            },
            State::HeaderField => {
                match bytes.next().unwrap() {
                    Ok(b' ') | Err(_) => return Err(Error::InvalidHeaderField),
                    Ok(b':') => { state = State::HeaderFieldParsed },
                    Ok(byte) => { current_header_field.push(byte as char) },
                }
            },
            State::HeaderFieldParsed => {
                match *bytes.peek().unwrap() {
                    Ok(b' ') => { let _ = bytes.next().unwrap(); },
                    Err(_) => return Err(Error::InvalidHeaderValue),
                    Ok(_) => { state = State::HeaderValueFirstByte },
                }
            },
            State::HeaderValueFirstByte => {
                match bytes.next().unwrap() {
                    Ok(b' ') | Ok(b':') | Ok(b'\r') | Ok(b'\n') | Err(_) => return Err(Error::InvalidHeaderValue),
                    Ok(byte) => {
                        current_header_value.push(byte as char);
                        state = State::HeaderValue;
                    }
                }
            },
            State::HeaderValue => {
                match bytes.next().unwrap() {
                    Ok(b'\r') => { state = State::HeaderValueAlmostParsed },
                    Ok(b':') | Ok(b'\n') | Err(_) => return Err(Error::InvalidHeaderValue),
                    Ok(byte) => { current_header_value.push(byte as char) },
                }
            },
            State::HeaderValueAlmostParsed => {
                match bytes.next().unwrap() {
                    Ok(b'\n') => {
                        let field = mem::replace(&mut current_header_field, String::new());
                        let value = mem::replace(&mut current_header_value, String::new());
                        headers.insert(Header::from(&*field), value);
                        state = State::HeaderFieldFirstByte;
                    },
                    _ => return Err(Error::InvalidHeaderValue),
                }
            },
            State::HeadersAlmostParsed => {
                match bytes.next().unwrap() {
                    Ok(b'\n') => { state = State::HeadersParsed },
                    _ => return Err(Error::InvalidHeaders),
                }
            }
            State::HeadersParsed => return Ok(headers),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::parse;
    use http::Error;

    #[test]
    fn test_no_header() {
        let mut line: &[u8] = b"\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let headers = parsed.unwrap();
        assert_eq!(0, headers.len());
    }

    #[test]
    fn test_one_line_header() {
        let mut line: &[u8] = b"Header-Field: value\r\n\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let headers = parsed.unwrap();
        assert_eq!(1, headers.len());

        let header = headers.get(&"Header-Field".into());
        assert_eq!(Some(&"value".to_string()), header);
    }

    #[test]
    fn test_two_lines_header() {
        let mut line: &[u8] = b"Header-Field: value\r\nOther-Field: other value\r\n\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let headers = parsed.unwrap();
        assert_eq!(2, headers.len());

        let header = headers.get(&"Header-Field".into());
        assert_eq!(Some(&"value".to_string()), header);

        let header = headers.get(&"Other-Field".into());
        assert_eq!(Some(&"other value".to_string()), header);
    }

    #[test]
    fn test_header_field_starting_with_whitespace() {
        let mut line: &[u8] = b" Header-Field: value\r\n\r\n";

        let parsed = parse(&mut line);
        assert_eq!(Err(Error::InvalidHeaderField), parsed);
    }

    #[test]
    fn test_header_field_starting_with_colon() {
        let mut line: &[u8] = b": value\r\n\r\n";

        let parsed = parse(&mut line);
        assert_eq!(Err(Error::InvalidHeaderField), parsed);
    }

    #[test]
    fn test_header_field_containing_whitespace() {
        let mut line: &[u8] = b"Header Field: value\r\n\r\n";

        let parsed = parse(&mut line);
        assert_eq!(Err(Error::InvalidHeaderField), parsed);
    }

    #[test]
    fn test_header_value_starting_with_whitespace() {
        let mut line: &[u8] = b"Header-Field:    value\r\n\r\n";

        let parsed = parse(&mut line);
        assert_eq!(true, parsed.is_ok());

        let headers = parsed.unwrap();
        assert_eq!(Some(&"value".to_string()), headers.get(&"Header-Field".into()));
    }
}