commit rust backend
This commit is contained in:
parent
db6bc13a49
commit
02d5fa4b8e
1
rs-backend/.gitignore
vendored
Normal file
1
rs-backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
7
rs-backend/Cargo.lock
generated
Normal file
7
rs-backend/Cargo.lock
generated
Normal file
@ -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"
|
8
rs-backend/Cargo.toml
Normal file
8
rs-backend/Cargo.toml
Normal file
@ -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]
|
39
rs-backend/src/main.rs
Normal file
39
rs-backend/src/main.rs
Normal file
@ -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(())
|
||||||
|
}
|
136
rs-backend/src/parse/client.rs
Normal file
136
rs-backend/src/parse/client.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
fn read_until_whitespace(
|
||||||
|
stream_buffer: &mut [u8],
|
||||||
|
stream_initial_index: usize,
|
||||||
|
) -> super::Result<String> {
|
||||||
|
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<HttpStart> {
|
||||||
|
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<HttpHeader> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
20
rs-backend/src/parse/error.rs
Normal file
20
rs-backend/src/parse/error.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Error {
|
||||||
|
InvalidHeader,
|
||||||
|
InvalidString,
|
||||||
|
Io,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::string::FromUtf8Error> for Error {
|
||||||
|
fn from(_: std::string::FromUtf8Error) -> Self {
|
||||||
|
Error::InvalidString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(_: std::io::Error) -> Self {
|
||||||
|
Error::Io
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
4
rs-backend/src/parse/mod.rs
Normal file
4
rs-backend/src/parse/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod error;
|
||||||
|
pub use client::*;
|
||||||
|
pub use error::*;
|
Loading…
Reference in New Issue
Block a user