diff --git a/Cargo.lock b/Cargo.lock index acf46b7..9ca3c78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,22 @@ name = "fstools" version = "0.1.0" [[package]] -name = "stdsrv" +name = "gravel_cli" version = "0.1.0" dependencies = [ "cracked_md", - "fstools", + "slogger", + "stdsrv", +] + +[[package]] +name = "slogger" +version = "0.1.0" + +[[package]] +name = "stdsrv" +version = "0.1.0" +dependencies = [ + "fstools", + "slogger", ] diff --git a/Cargo.toml b/Cargo.toml index 6ec0d5e..fe7ea9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "stdsrv", "cracked_md", "fstools", + "gravel_cli", "slogger", ] resolver = "3" diff --git a/gravel_cli/Cargo.toml b/gravel_cli/Cargo.toml new file mode 100644 index 0000000..e1d42f6 --- /dev/null +++ b/gravel_cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gravel_cli" +version = "0.1.0" +edition = "2024" + + +[[bin]] +name = "gravel" +path = "src/main.rs" + +[dependencies] +cracked_md = { path = "../cracked_md" } +stdsrv = { path = "../stdsrv" } +slogger = { path = "../slogger" } diff --git a/stdsrv/src/args.rs b/gravel_cli/src/args.rs similarity index 51% rename from stdsrv/src/args.rs rename to gravel_cli/src/args.rs index 075a2e0..2e65e45 100644 --- a/stdsrv/src/args.rs +++ b/gravel_cli/src/args.rs @@ -1,18 +1,20 @@ //! Simple and program specific command line argument parsing solution. -use crate::error::Error; -use crate::error::ErrorKind; -use std::path::PathBuf; -use std::sync::OnceLock; +// todo: refactor to [] -pub static VERBOSE: OnceLock = OnceLock::new(); +use slogger::{LOG_LEVEL, Level}; + +use crate::error::Error; +use std::net::Ipv4Addr; +use std::path::PathBuf; pub struct ProgramArgs { pub outdir: PathBuf, pub indir: PathBuf, pub generate: bool, pub force: bool, - pub addr: String, + pub addr: Ipv4Addr, + pub port: u16, pub verbose: bool, } @@ -23,7 +25,8 @@ impl Default for ProgramArgs { outdir: PathBuf::from("./html"), generate: false, force: false, - addr: "0.0.0.0:8080".to_string(), + addr: Ipv4Addr::UNSPECIFIED, + port: 8080, verbose: false, } } @@ -39,30 +42,43 @@ impl TryFrom for ProgramArgs { "-i" => { a.indir = value .next() - .ok_or(Error::new( - ErrorKind::CommandLineArgsParse, - "Expected input directory after option `-i`", + .ok_or(Error::CommandLineArgsParse( + "Expected input directory after option `-i`".to_string(), ))? .into(); } "-a" => { - a.addr = value.next().ok_or(Error::new( - ErrorKind::CommandLineArgsParse, - "Expected listener address after option `-a`", + let addr_string = value.next().ok_or(Error::CommandLineArgsParse( + "Expected listener IPv4 address after option `-a`".to_string(), ))?; + a.addr = Ipv4Addr::parse_ascii(addr_string.as_bytes()).map_err(|_e| { + Error::CommandLineArgsParse( + "Invalid IPv4 address after option `-a`".to_string(), + ) + })?; + } + "-p" => { + let port_string = value.next().ok_or(Error::CommandLineArgsParse( + "Expected listener port after option `-p`".to_string(), + ))?; + a.port = port_string.parse().map_err(|_e| { + Error::CommandLineArgsParse( + "Invalid 16-bit port number after option `-p`".to_string(), + ) + })?; } "-g" => a.generate = true, "-f" => a.force = true, "-v" => { a.verbose = true; - VERBOSE.get_or_init(|| true); + LOG_LEVEL.get_or_init(|| Level::Debug); } _ => { a.outdir = v.into(); } } } - VERBOSE.get_or_init(|| false); + LOG_LEVEL.get_or_init(|| Level::Info); Ok(a) } } diff --git a/gravel_cli/src/error.rs b/gravel_cli/src/error.rs new file mode 100644 index 0000000..dca13f5 --- /dev/null +++ b/gravel_cli/src/error.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum Error { + Server(stdsrv::error::Error), + MdParse(cracked_md::Error), + CommandLineArgsParse(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Server(e) => e.fmt(f), + Error::MdParse(e) => e.fmt(f), + Error::CommandLineArgsParse(s) => write!(f, "{s}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(value: cracked_md::Error) -> Self { + Self::MdParse(value) + } +} + +impl From for Error { + fn from(value: stdsrv::error::Error) -> Self { + Self::Server(value) + } +} diff --git a/gravel_cli/src/main.rs b/gravel_cli/src/main.rs new file mode 100644 index 0000000..c4e7c35 --- /dev/null +++ b/gravel_cli/src/main.rs @@ -0,0 +1,17 @@ +#![feature(addr_parse_ascii, never_type)] + +use args::ProgramArgs; +use cracked_md::generate; +use error::Error; +//use slogger::{LOG_LEVEL, Level}; +use stdsrv::serve; + +mod args; +mod error; + +fn main() -> Result { + let args = ProgramArgs::try_from(std::env::args())?; + + generate(&args.indir, &args.outdir, args.force)?; + serve(args.addr, args.port, args.outdir)?; +} diff --git a/slogger/Cargo.toml b/slogger/Cargo.toml new file mode 100644 index 0000000..d49e963 --- /dev/null +++ b/slogger/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "slogger" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/stdsrv/src/logger.rs b/slogger/src/lib.rs similarity index 76% rename from stdsrv/src/logger.rs rename to slogger/src/lib.rs index 81a2ea0..14bf304 100644 --- a/stdsrv/src/logger.rs +++ b/slogger/src/lib.rs @@ -1,8 +1,9 @@ -use std::fmt::Display; +use std::{fmt::Display, sync::OnceLock}; + +pub static LOG_LEVEL: OnceLock = OnceLock::new(); -#[derive(Debug)] #[allow(dead_code)] -#[derive(PartialEq)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Level { Error, Warn, @@ -26,12 +27,10 @@ impl Display for Level { } /// A logging macro. Takes a [`Level`] and a formatted string. -/// -/// [`Level`]: ./logger/enum.Level.html #[macro_export] macro_rules! log { ($level:expr, $($arg:tt)*) => {{ - if $level != Level::Debug || crate::args::VERBOSE.get().unwrap().to_owned() { + if &$level <= $crate::LOG_LEVEL.get().unwrap_or(&$crate::Level::Info) { println!( "{} {}:{}:{}: {}", $level, @@ -43,3 +42,5 @@ macro_rules! log { } }}; } + +// todo: implement clean verbose/short logging diff --git a/stdsrv/Cargo.toml b/stdsrv/Cargo.toml index c969a6d..0b4bd64 100644 --- a/stdsrv/Cargo.toml +++ b/stdsrv/Cargo.toml @@ -3,11 +3,7 @@ name = "stdsrv" version = "0.1.0" edition = "2024" -[[bin]] -name = "gravel" -path = "src/main.rs" - # local dependencies [dependencies] -cracked_md = { path = "../cracked_md" } fstools = { path = "../fstools" } +slogger = { path = "../slogger" } diff --git a/stdsrv/src/error.rs b/stdsrv/src/error.rs index d47e647..6a20369 100644 --- a/stdsrv/src/error.rs +++ b/stdsrv/src/error.rs @@ -27,6 +27,7 @@ pub struct Error { } impl Error { + #[must_use] pub fn new(kind: ErrorKind, msg: &str) -> Self { Self { kind, diff --git a/stdsrv/src/fileserver.rs b/stdsrv/src/fileserver.rs index bfa4ce6..413ffda 100644 --- a/stdsrv/src/fileserver.rs +++ b/stdsrv/src/fileserver.rs @@ -1,8 +1,8 @@ //! A simple server implementation that just responds with the contents of the file requested in //! the provided directory. -use std::fs; use std::path::PathBuf; +use std::{fs, path::Path}; use crate::{ error::{Error, ErrorKind, Result}, @@ -16,7 +16,7 @@ pub struct FileServer { } impl FileServer { - pub fn new(root: &PathBuf) -> Result { + pub fn new(root: &Path) -> Result { if !root.is_dir() { return Err(Error::new( ErrorKind::DirNotFound, diff --git a/stdsrv/src/http_stream.rs b/stdsrv/src/http_stream.rs deleted file mode 100644 index 1765be8..0000000 --- a/stdsrv/src/http_stream.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::error::ErrorKind; -use crate::log; -use crate::logger::Level; -use std::io::Read; -use std::net::{TcpListener, TcpStream}; - -use crate::{error::Error, request::HttpRequest}; - -pub struct HttpStream { - tcp_listener: TcpListener, -} - -impl HttpStream { - pub fn new(addr: &str) -> Self { - let tcp_listener = TcpListener::bind(addr) - .unwrap_or_else(|e| panic!("Failed to bind on address `{}`: {}", addr, e)); - log!(Level::Info, "Listening on `{}`", addr); - - Self { tcp_listener } - } -} - -impl Iterator for HttpStream { - type Item = (HttpRequest, TcpStream); - - fn next(&mut self) -> Option { - // safe to unwrap, because Incoming never returns None - let mut stream = self.tcp_listener.incoming().next().unwrap().ok()?; - - let mut buf = [0; 1024]; - let _read_bytes = stream - .read(&mut buf) - .or(Err(Error::new( - ErrorKind::StreamReadFailed, - "Reading from TCP stream failed", - ))) - .ok()?; - Some(( - String::from_utf8_lossy(&buf[..]).trim().try_into().ok()?, - stream, - )) - } -} diff --git a/stdsrv/src/main.rs b/stdsrv/src/lib.rs similarity index 73% rename from stdsrv/src/main.rs rename to stdsrv/src/lib.rs index 3595a8a..1c67923 100644 --- a/stdsrv/src/main.rs +++ b/stdsrv/src/lib.rs @@ -1,31 +1,36 @@ //! A simple web server with 0 dependencies (other than Rust's stdlib). -//! Documentation is a work in progress, go see my webpage at [jlux.dev](https://jlux.dev). + +#![feature(never_type)] +#![allow(dead_code)] use std::{ io::{BufReader, BufWriter}, - net::TcpListener, - process, + net::{Ipv4Addr, TcpListener}, + path::PathBuf, }; -use args::ProgramArgs; -use cracked_md::generate; +use error::Error; use fileserver::FileServer; -use logger::Level; use request::HttpRequest; use responder::Responder; +use slogger::{Level, log}; -mod args; -mod error; +pub mod error; mod fileserver; mod http_header; -//mod http_stream; -mod logger; mod request; mod responder; mod response; -/// Entrypoint to the program. -fn main() -> Result<(), Box> { +/// Opens a file server on a specified address and port which serves all files in dir. +/// +/// # Errors +/// Errors that come up while serving files. Look at [`Error`]. +/// +/// # Panics +/// Never. Added to allow compiler to check for ! type. +pub fn serve(addr: Ipv4Addr, port: u16, dir: PathBuf) -> Result { + /* let args: ProgramArgs = std::env::args().try_into()?; if args.generate { @@ -51,18 +56,20 @@ fn main() -> Result<(), Box> { } } - let listener = TcpListener::bind(&args.addr)?; - log!(Level::Info, "Listening on addr `{}`", &args.addr); + */ + let listener = TcpListener::bind((addr, port))?; + log!(Level::Info, "Listening on addr `{}:{}`", addr, port); + // todo: refactor this for stream in listener.incoming() { match stream { Ok(stream) => { - let outdir = args.outdir.clone(); + let outdir = dir.clone(); std::thread::spawn(move || { log!(Level::Debug, "TcpStream handler spawned"); let mut reader = BufReader::new(&stream); let mut writer = BufWriter::new(&stream); - let server = match FileServer::new(&outdir) { + let server = match FileServer::new(outdir.as_path()) { Ok(s) => s, Err(_e) => return, }; @@ -82,5 +89,5 @@ fn main() -> Result<(), Box> { } } - Ok(()) + panic!("Code shouldn't get to here"); } diff --git a/stdsrv/src/request.rs b/stdsrv/src/request.rs index ccb76a5..5dfdacd 100644 --- a/stdsrv/src/request.rs +++ b/stdsrv/src/request.rs @@ -2,8 +2,7 @@ use crate::error::{Error, ErrorKind, Result}; use crate::http_header::HttpHeaders; -use crate::log; -use crate::logger::Level; +use slogger::{Level, log}; use std::fmt::Display; use std::io::{BufRead, BufReader}; use std::net::TcpStream; diff --git a/stdsrv/src/response.rs b/stdsrv/src/response.rs index dc15c6c..5424ff2 100644 --- a/stdsrv/src/response.rs +++ b/stdsrv/src/response.rs @@ -2,8 +2,7 @@ use crate::error::Result; use crate::http_header::HttpHeaders; -use crate::log; -use crate::logger::Level; +use slogger::{Level, log}; use std::{fmt::Display, io::Write}; /// Macro for generating Http status codes (AI generated).