httpio 0.2.4

A transport-agnostic, async HTTP/1.1 client library for any runtime.
Documentation
use crate::enums::char_set::CharSet;
use crate::enums::header::http_header::HttpHeader;
use crate::enums::http_body::HttpBody;
use crate::enums::http_error::{HttpError, HttpErrorKind};
use crate::structures::header::header_list::HttpHeaderList;
use crate::utils::parse_util;
use async_regex::CRLF;
use core::fmt;
use futures::{AsyncBufRead, AsyncBufReadExt};
use std::fmt::Write;

pub struct HttpMessage {
    pub start_line: String,
    pub headers: HttpHeaderList,
    pub body: HttpBody,
}

impl HttpMessage {
    pub fn new<T: ToString>(start_line: T, headers: HttpHeaderList, body: HttpBody) -> HttpMessage {
        HttpMessage {
            start_line: start_line.to_string(),
            headers,
            body,
        }
    }

    pub async fn read<R: AsyncBufRead + Unpin>(mut reader: R) -> Result<HttpMessage, HttpError> {
        let mut start_line = String::new();
        reader.read_line(&mut start_line).await?;
        let mut headers = HttpHeaderList::read(&mut reader).await?;
        let body = HttpBody::read(reader, &mut headers).await?;

        Ok(HttpMessage {
            start_line: start_line.trim_end().to_string(),
            headers,
            body,
        })
    }

    pub fn get_header(&self, key: &str) -> Option<&HttpHeader> {
        self.headers.get(key)
    }

    pub fn as_bytes(&self, add_content_length: bool) -> Box<[u8]> {
        let body = self.body.as_bytes();

        let mut headers_str = String::with_capacity(256);
        let mut first = true;
        for (_, header) in self.headers.iter() {
            if !first {
                headers_str.push_str("\r\n");
            }
            let _ = write!(headers_str, "{}", header);
            first = false;
        }
        if add_content_length {
            if !first {
                headers_str.push_str("\r\n");
            }
            let _ = write!(
                headers_str,
                "{}:{}",
                crate::utils::http_header_field_name::CONTENT_LENGTH,
                body.len()
            );
        }
        let headers = headers_str.as_bytes();
        let mut result = Vec::with_capacity(self.start_line.len() + headers.len() + body.len() + 6);
        result.extend_from_slice(self.start_line.as_bytes());
        result.extend_from_slice(CRLF);
        result.extend_from_slice(headers);
        result.extend_from_slice(CRLF);
        result.extend_from_slice(CRLF);
        result.extend_from_slice(&body);
        result.into_boxed_slice()
    }
}

impl fmt::Display for HttpMessage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let charset = self.headers.get_charset(CharSet::Iso88591);
        let bytes = self.as_bytes(false);
        let str = match parse_util::bytes_to_string(&bytes, charset) {
            Ok(str) => str,
            Err(error) if matches!(error.kind, HttpErrorKind::UnsupportedCharset) => {
                parse_util::bytes_to_string(&bytes, CharSet::Iso88591).unwrap()
            }
            _ => unreachable!(),
        };
        write!(f, "{}", str)
    }
}

impl fmt::Debug for HttpMessage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

#[cfg(test)]
mod tests {
    use futures::executor::block_on;
    use std::str::from_utf8;

    use super::*;

    #[test]
    fn test_read_content_msg() -> Result<(), HttpError> {
        let msg = r###"POST /test HTTP/1.1
Host: foo.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

field1=value1&field2=value2"###;

        let orig_msg = msg.replace("\n", from_utf8(&CRLF)?);
        let msg = block_on(async { HttpMessage::read(&mut orig_msg.as_bytes()).await })?;
        assert!(matches!(msg.body, HttpBody::Content(_)));
        assert_eq!(msg.body.get_content().len(), 27);
        Ok(())
    }

    #[test]
    fn test_read_mulipart_form_body() -> Result<(), HttpError> {
        let msg = r###"POST /test HTTP/1.1
Host: foo.example
Content-Type: multipart/form-data;boundary="simple boundary"

This is the preamble.  It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--simple boundary

This is 1 implicitly typed plain ASCII text.
It does NOT end with a linebreak.
--simple boundary
Content-type: text/plain; charset=us-ascii

This is 2 explicitly typed plain ASCII text.
It DOES end with a linebreak.

--simple boundary--
This is the epilogue.  It is also to be ignored."###;

        let msg = msg.replace("\n", from_utf8(&CRLF)?);
        let msg = block_on(async { HttpMessage::read(&mut msg.as_bytes()).await })?;
        assert!(matches!(msg.body, HttpBody::Multipart(_)));
        let multipart = msg.body.get_multipart().unwrap();
        assert_eq!(multipart.parts.len(), 2);
        assert_eq!(multipart.parts[0].headers.len(), 0);
        assert_eq!(multipart.parts[1].headers.len(), 1);
        Ok(())
    }
}