francoisgib_webserver 1.0.3

HTTP Webserver
Documentation
mod test;

use std::str::FromStr;

use smallvec::SmallVec;
use tokio::io::{AsyncBufRead, AsyncBufReadExt};

use crate::{
    config::PRE_ALLOCATED_REQUEST_HEADERS,
    http::{errors::HttpError, headers::HeaderEntry, methods::HttpMethod},
};

fn parse_uri<'a>(buffer: &'a str) -> Result<(&'a str, &'a str), HttpError> {
    let (uri, rest) = parse_to_delimiter(" ", buffer, "Error parsing URI")?;

    if !uri.starts_with('/') {
        Err(HttpError::BadRequest)
    } else {
        Ok((uri, rest))
    }
}

fn parse_method<'a>(buffer: &'a str) -> Result<(HttpMethod, &'a str), HttpError> {
    let (method_str, rest) = parse_to_delimiter(" ", &buffer, "Error parsing method")?;
    let method = HttpMethod::from_str(&method_str)
        .map_err(|_| format!("Invalid method: \"{}\".", method_str))
        .map_err(|_| HttpError::NotImplemented)?;
    Ok((method, rest))
}

fn parse_http_version<'a>(buffer: &'a str) -> Result<(u8, u8), HttpError> {
    let (http_literal, version_str) =
        parse_to_delimiter("/", buffer, "Error parsing HTTP version")?;

    if http_literal.trim() != "HTTP" {
        return Err(format!("Invalid HTTP version format: {}", buffer))
            .map_err(|_| HttpError::BadRequest)?;
    }

    let version_str = version_str
        .strip_suffix("\r\n")
        .ok_or("Missing end line in request line")
        .map_err(|_| HttpError::BadRequest)?;

    let (major, minor) = version_str
        .split_once('.')
        .or(Some((version_str, "0")))
        .ok_or(HttpError::BadRequest)?;

    let major = major.parse::<u8>().map_err(|_| HttpError::BadRequest)?;
    let minor = minor.parse::<u8>().map_err(|_| HttpError::BadRequest)?;

    Ok((major, minor))
}

pub async fn parse_request_line<R: AsyncBufRead + Unpin>(
    reader: &mut R,
) -> Result<(HttpMethod, String, (u8, u8)), HttpError> {
    let mut buffer = String::with_capacity(128);
    reader
        .read_line(&mut buffer)
        .await
        .map_err(|error| format!("Error parsing request line: {}.", error))
        .inspect_err(|error| println!("{error}"))
        .map_err(|_| HttpError::BadRequest)?;

    let (method, rest) = parse_method(&buffer)?;
    let (uri, version_part) = parse_uri(rest)?;
    let (major, minor) = parse_http_version(version_part)?;

    Ok((method, uri.to_owned(), (major, minor)))
}

pub async fn parse_headers<R: AsyncBufRead + Unpin>(
    reader: &mut R,
) -> Result<SmallVec<[HeaderEntry; PRE_ALLOCATED_REQUEST_HEADERS]>, String> {
    let mut headers = SmallVec::new();
    let mut buffer = String::with_capacity(256);

    loop {
        buffer.clear();
        let bytes_read = reader
            .read_line(&mut buffer)
            .await
            .map_err(|e| e.to_string())?;

        if bytes_read == 0 {
            return Err("Unexpected EOF".to_string());
        }

        if buffer == "\r\n" {
            break;
        }

        let header = buffer
            .strip_suffix("\r\n")
            .ok_or("Missing end line in header")?
            .trim()
            .to_string();

        match HeaderEntry::from_str(header.as_str()) {
            Ok(header_entry) => {
                headers.push(header_entry);
            }
            _ => {}
        }
    }

    Ok(headers)
}

fn parse_to_delimiter<'a, 'b>(
    delimiter: &'a str,
    content: &'a str,
    error_message: &'b str,
) -> Result<(&'a str, &'a str), HttpError> {
    match content.split_once(delimiter) {
        Some((part, rest)) => Ok((part, rest)),
        None => {
            println!("{error_message}");
            Err(HttpError::BadRequest)
        }
    }
}