From 02d5fa4b8e05e5869e8cc774c4e17bbe77c2da26 Mon Sep 17 00:00:00 2001 From: Theis Pieter Hollebeek Date: Thu, 9 Feb 2023 15:08:13 +0100 Subject: [PATCH] commit rust backend --- rs-backend/.gitignore | 1 + rs-backend/Cargo.lock | 7 ++ rs-backend/Cargo.toml | 8 ++ rs-backend/src/main.rs | 39 ++++++++++ rs-backend/src/parse/client.rs | 136 +++++++++++++++++++++++++++++++++ rs-backend/src/parse/error.rs | 20 +++++ rs-backend/src/parse/mod.rs | 4 + 7 files changed, 215 insertions(+) create mode 100644 rs-backend/.gitignore create mode 100644 rs-backend/Cargo.lock create mode 100644 rs-backend/Cargo.toml create mode 100644 rs-backend/src/main.rs create mode 100644 rs-backend/src/parse/client.rs create mode 100644 rs-backend/src/parse/error.rs create mode 100644 rs-backend/src/parse/mod.rs diff --git a/rs-backend/.gitignore b/rs-backend/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/rs-backend/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/rs-backend/Cargo.lock b/rs-backend/Cargo.lock new file mode 100644 index 0000000..f8d9113 --- /dev/null +++ b/rs-backend/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "rs-backend" +version = "0.1.0" diff --git a/rs-backend/Cargo.toml b/rs-backend/Cargo.toml new file mode 100644 index 0000000..cf298f2 --- /dev/null +++ b/rs-backend/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rs-backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/rs-backend/src/main.rs b/rs-backend/src/main.rs new file mode 100644 index 0000000..c270d8d --- /dev/null +++ b/rs-backend/src/main.rs @@ -0,0 +1,39 @@ +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; + +mod parse; + +fn parse_client(stream_buffer: &mut [u8]) -> parse::Result<()> { + let start = parse::http_start(stream_buffer)?; + loop { + match parse::http_header(stream_buffer, start.total_length) { + Ok(header) => { + todo!(); + } + Err(err) => match err { + parse::Error::InvalidHeader => { + break; + } + err => return Err(err), + }, + } + } + Ok(()) +} + +fn handle_client(mut stream: TcpStream) -> parse::Result<()> { + let mut stream_buffer = Vec::from([0u8; 4096]); + let bytes_read = stream.read(&mut stream_buffer)?; + stream_buffer.truncate(bytes_read); + parse_client(&mut stream_buffer) +} + +fn main() -> std::io::Result<()> { + let listener = TcpListener::bind("127.0.0.1:8080")?; + for stream in listener.incoming() { + if let Err(err) = handle_client(stream?) { + println!("error occurred: {err:#?}"); + }; + } + Ok(()) +} diff --git a/rs-backend/src/parse/client.rs b/rs-backend/src/parse/client.rs new file mode 100644 index 0000000..ef9bed0 --- /dev/null +++ b/rs-backend/src/parse/client.rs @@ -0,0 +1,136 @@ +fn read_until_whitespace( + stream_buffer: &mut [u8], + stream_initial_index: usize, +) -> super::Result { + let buffer = String::from_utf8( + (stream_initial_index..stream_buffer.len()) + .map_while(|stream_index| { + let byte = stream_buffer[stream_index]; + if byte == b' ' || byte == b'\r' && stream_buffer[stream_index + 1] == b'\n' { + return None; + } + + Some(byte) + }) + .collect(), + )?; + + Ok(buffer) +} + +pub struct HttpStart { + pub method: String, + pub path: String, + pub version: String, + pub total_length: usize, +} + +pub fn http_start(stream_buffer: &mut [u8]) -> super::Result { + let mut total_length = 0; + let method = read_until_whitespace(stream_buffer, total_length)?; + total_length += method.len() + 1; + let path = read_until_whitespace(stream_buffer, total_length)?; + total_length += path.len() + 1; + let version = read_until_whitespace(stream_buffer, total_length)?; + total_length += version.len() + 2; // CRLF + Ok(HttpStart { + method, + path, + version, + total_length, + }) +} + +pub struct HttpHeader { + pub key: String, + pub content: String, + pub total_length: usize, +} + +pub fn http_header( + stream_buffer: &mut [u8], + stream_initial_index: usize, +) -> super::Result { + if read_until_whitespace(stream_buffer, stream_initial_index)?.is_empty() { + return Err(super::Error::InvalidHeader); + } + + let key = String::from_utf8( + (stream_initial_index..stream_buffer.len()) + .map_while(|stream_index| { + let byte = stream_buffer[stream_index]; + if byte == b':' { + None + } else { + Some(byte) + } + }) + .collect(), + )?; + + let content = String::from_utf8( + (stream_initial_index + key.len() + 2..stream_buffer.len()) + .map_while(|stream_index| { + let byte = stream_buffer[stream_index]; + if byte == b'\r' && stream_buffer[stream_index + 1] == b'\n' { + None + } else { + Some(byte) + } + }) + .collect(), + )?; + + let total_length = key.len() + content.len() + 4; // ': ' + '\r\n' + + Ok(HttpHeader { + key, + content, + total_length, + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn valid_http_header() { + let mut buffer = b"Content-Type: application/json\r\n".to_owned(); + let len = buffer.len(); + let header = http_header(&mut buffer, 0).expect("should not fail with valid input"); + assert_eq!(header.total_length, len, "total length should be {len}"); + assert_eq!( + &header.content, "application/json", + "header content should be application/json" + ); + assert_eq!( + &header.key, "Content-Type", + "header key should be Content-Type" + ); + } + + #[test] + fn invalid_http_header() { + let mut buffer = b"\r\n".to_owned(); + let result = http_header(&mut buffer, 0) + .err() + .expect("should fail with invalid input"); + assert_eq!( + result, + crate::parse::Error::InvalidHeader, + "should return ParseError::InvalidHeader on invalid header" + ); + } + + #[test] + fn valid_http_start() { + let mut buffer = b"GET /resource HTTP/1.1\r\n".to_owned(); + let len = buffer.len(); + let start = http_start(&mut buffer).expect("should not fail with valid input"); + assert_eq!(start.total_length, len, "total length should be {len}"); + assert_eq!(&start.method, "GET", "method should be GET"); + assert_eq!(&start.path, "/resource", "path should be /resource"); + assert_eq!(&start.version, "HTTP/1.1", "version should be HTTP/1.1"); + } +} diff --git a/rs-backend/src/parse/error.rs b/rs-backend/src/parse/error.rs new file mode 100644 index 0000000..1c70a44 --- /dev/null +++ b/rs-backend/src/parse/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + InvalidHeader, + InvalidString, + Io, +} + +impl From for Error { + fn from(_: std::string::FromUtf8Error) -> Self { + Error::InvalidString + } +} + +impl From for Error { + fn from(_: std::io::Error) -> Self { + Error::Io + } +} + +pub type Result = std::result::Result; diff --git a/rs-backend/src/parse/mod.rs b/rs-backend/src/parse/mod.rs new file mode 100644 index 0000000..db4b864 --- /dev/null +++ b/rs-backend/src/parse/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod error; +pub use client::*; +pub use error::*;