Compare commits

...

7 Commits

Author SHA1 Message Date
2d4f611de1 begun development for single file generation
All checks were successful
Test the running changes / Test (push) Successful in 40s
2025-11-26 19:31:21 +02:00
1c7504e3e0 added special character escaping to md
All checks were successful
Test the running changes / Test (push) Successful in 42s
2025-11-23 22:31:24 +02:00
7d602fffba added cargo-tarpaulin to flake, added some tests to cracked_md
All checks were successful
Test the running changes / Test (push) Successful in 40s
2025-11-23 20:52:05 +02:00
1aa6af3b1a refactored cli and logging
All checks were successful
Test the running changes / Test (push) Successful in 41s
2025-11-21 01:37:19 +02:00
f68b1bb276 modified toolchain, added helix editor 2025-11-20 23:57:53 +02:00
7b300987b7 refactor: split cli and logging to separate crates
All checks were successful
Test the running changes / Test (push) Successful in 42s
2025-11-18 23:41:45 +02:00
33bfff2e98 bugfix, implemented verbose logging cli option
Some checks failed
Test the running changes / Test (push) Failing after 38s
2025-11-17 00:48:23 +02:00
25 changed files with 492 additions and 192 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
html/ pebbles/
web/ site/
target/ target/
result result
tarpaulin-report.html

18
Cargo.lock generated
View File

@@ -7,6 +7,7 @@ name = "cracked_md"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"fstools", "fstools",
"slogger",
] ]
[[package]] [[package]]
@@ -14,9 +15,22 @@ name = "fstools"
version = "0.1.0" version = "0.1.0"
[[package]] [[package]]
name = "stdsrv" name = "gravel_cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cracked_md", "cracked_md",
"fstools", "slogger",
"stdsrv",
]
[[package]]
name = "slogger"
version = "0.1.0"
[[package]]
name = "stdsrv"
version = "0.1.0"
dependencies = [
"fstools",
"slogger",
] ]

View File

@@ -3,6 +3,7 @@ members = [
"stdsrv", "stdsrv",
"cracked_md", "cracked_md",
"fstools", "fstools",
"gravel_cli", "slogger",
] ]
resolver = "3" resolver = "3"

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies] [dependencies]
fstools = { path = "../fstools" } fstools = { path = "../fstools" }
slogger = { path = "../slogger" }

View File

@@ -16,15 +16,10 @@ pub enum Block {
language: Option<String>, language: Option<String>,
content: String, content: String,
}, },
List(Vec<ListItem>), List(Vec<Block>),
Quote(Vec<Block>), Quote(Vec<Block>),
} }
#[derive(Debug, Clone, PartialEq)]
pub struct ListItem {
pub blocks: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Inline { pub enum Inline {
Text(String), Text(String),

View File

@@ -1,16 +1,18 @@
//! A "Markdown" parser and HTML generator. Part of a static site generator `marksmith-rs`. //! A "Markdown" parser and HTML generator. Part of a static site generator `marksmith-rs`.
//! Not following any standards, only vibes. //! Not following any standards, only vibes.
#![deny(unused_imports)] #![deny(dead_code, unused_imports)]
#![allow(clippy::needless_pass_by_value)] #![allow(clippy::needless_pass_by_value)]
use fstools::crawl_fs; use fstools::crawl_fs;
use parser::parse; use parser::parse;
use slogger::{Level, log};
use std::{ use std::{
fmt::Display, fmt::Display,
fs::{self, File}, fs::{self, File},
io::Write, io::Write,
path::PathBuf, path::PathBuf,
time::Instant,
}; };
use to_html::ToHtml; use to_html::ToHtml;
@@ -104,16 +106,7 @@ impl std::error::Error for MdParseError {}
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
InDirIsNotDir, FSError(String),
OutDirIsNotEmpty,
OutDirIsNotDir,
OutDirFileDeleteNotAllowed,
OutDirDirectoryInPlaceOfFile,
FileRead,
DirRead,
FileWrite,
FileCreate,
DirCreate,
Parse(MdParseError), Parse(MdParseError),
} }
@@ -139,11 +132,14 @@ type Result<T> = std::result::Result<T, crate::Error>;
/// # Errors /// # Errors
/// Anything wrong with reading files from the directories or parsing the files. /// Anything wrong with reading files from the directories or parsing the files.
pub fn generate(indir: &PathBuf, outdir: &PathBuf, force: bool) -> Result<()> { pub fn generate(indir: &PathBuf, outdir: &PathBuf, force: bool) -> Result<()> {
let start_time = Instant::now();
let mut generated_files = 0;
if !indir.is_dir() { if !indir.is_dir() {
Err(Error::InDirIsNotDir)?; Err(Error::FSError("In directory not found".to_string()))?;
} }
if !outdir.is_dir() { if !outdir.is_dir() {
Err(Error::OutDirIsNotDir)?; Err(Error::FSError("Out directory not found".to_string()))?;
} }
let files = crawl_fs(indir); let files = crawl_fs(indir);
@@ -151,34 +147,75 @@ pub fn generate(indir: &PathBuf, outdir: &PathBuf, force: bool) -> Result<()> {
let fullpath = indir.as_path().join(&path); let fullpath = indir.as_path().join(&path);
// read and parse md file // read and parse md file
let content = fs::read_to_string(&fullpath).map_err(|_e| Error::FileRead)?; let content = fs::read_to_string(&fullpath)
.map_err(|_e| Error::FSError(format!("File `{}` read error", path.display())))?;
let html = parse(&content)?.to_html(); let html = parse(&content)?.to_html();
// write html data to file // write html data to file
let mut newpath = outdir.to_owned(); let mut newpath = outdir.to_owned();
newpath.push(path); newpath.push(&path);
newpath.set_extension("html"); newpath.set_extension("html");
// check if path exists // check if path exists
if newpath.exists() { if newpath.exists() {
// remove if is file and if force, otherwise error // remove if is file and if force, otherwise error
if newpath.is_file() && force { if newpath.is_file() {
fs::remove_file(&newpath).map_err(|_e| Error::OutDirFileDeleteNotAllowed)?; if force {
fs::remove_file(&newpath).map_err(|_e| {
Error::FSError(format!("File `{}` deleting not allowed", newpath.display()))
})?;
} else { } else {
Err(Error::OutDirDirectoryInPlaceOfFile)?; Err(Error::FSError(
"File overwrite denied, enable force overwrite".to_string(),
))?;
}
} else {
Err(Error::FSError(format!(
"Directory `{}` in place of file in out directory",
newpath.display()
)))?;
} }
} }
//println!("About to write file '{}'", newpath.display()); //println!("About to write file '{}'", newpath.display());
let parent = newpath.parent().ok_or(Error::DirCreate)?; let parent = newpath.parent().ok_or(Error::FSError(format!(
fs::create_dir_all(parent).map_err(|_e| Error::DirCreate)?; "Access to parent directory of `{}` denied",
let mut newfile = File::create_new(newpath).map_err(|_e| Error::FileCreate)?; newpath.display()
)))?;
fs::create_dir_all(parent)
.map_err(|_e| Error::FSError("Creating directory tree failed".to_string()))?;
let mut newfile = File::create_new(&newpath).map_err(|_e| {
Error::FSError(format!("Creating file `{}` failed", &newpath.display()))
})?;
newfile newfile.write(html.as_bytes()).map_err(|_e| {
.write(html.as_bytes()) Error::FSError(format!("Writing to file `{}` failed", newpath.display()))
.map_err(|_e| Error::FileWrite)?; })?;
log!(
Level::Debug,
"File `{}` generation to `{}` successful",
path.display(),
newpath.display()
);
generated_files += 1;
} }
let time = start_time.elapsed();
let time_report = if time.as_micros() < 10000 {
format!("{} μs", time.as_micros())
} else {
format!("{} ms", time.as_millis())
};
log!(
Level::Info,
"Generated {} files in {} without reported errors",
generated_files,
time_report
);
Ok(()) Ok(())
} }

View File

@@ -133,6 +133,20 @@ mod test {
); );
} }
#[test]
fn code_block_content_after_end() {
let md = "```\necho hello\n```abc";
let doc_res = parse(md);
assert!(doc_res.is_err());
}
#[test]
fn code_block_no_terminating() {
let md = "```\nabc\n";
let doc_res = parse(md);
assert!(doc_res.is_err());
}
#[test] #[test]
fn rust_code_block() { fn rust_code_block() {
let md = "```rust\nfn main() {\n\tprintln!(\"Hello world!\");\n}\n```"; let md = "```rust\nfn main() {\n\tprintln!(\"Hello world!\");\n}\n```";

View File

@@ -48,6 +48,11 @@ pub fn parse_blocks(input: &str) -> Result<Vec<Block>, MdParseError> {
} }
*/ */
// unordered list TODO
if line_chars.parse_str("- ") {
todo!()
}
// code // code
if line_chars.parse_str("```") { if line_chars.parse_str("```") {
let lang_line: String = line_chars.collect(); let lang_line: String = line_chars.collect();

View File

@@ -36,12 +36,18 @@ pub fn parse_inlines(input: &str) -> Result<Vec<Inline>, MdParseError> {
_ => { _ => {
let mut text = String::new(); let mut text = String::new();
text.push(c); text.push(c);
let mut escaped = false;
while let Some(&nc) = chars.peek() { while let Some(&nc) = chars.peek() {
if matches!(nc, '*' | '_' | '`' | '[') { if matches!(nc, '*' | '_' | '`' | '[') && !escaped {
break; break;
} }
let c = chars.next().ok_or(MdParseError::new("a character", ""))?; let next_c = chars.next().ok_or(MdParseError::new("a character", ""))?;
text.push(c); if next_c == '\\' && !escaped {
escaped = true;
} else {
escaped = false;
text.push(next_c);
}
} }
inlines.push(Inline::Text(text)); inlines.push(Inline::Text(text));
} }
@@ -69,7 +75,14 @@ fn collect_until<I: Iterator<Item = char>>(
mod test { mod test {
use crate::ast::Inline; use crate::ast::Inline;
use super::parse_inlines; use super::{collect_until, parse_inlines};
#[test]
fn collect_until_without_end() {
let mut s = "abcdef".chars().peekable();
let res = collect_until(&mut s, '.');
assert!(res.is_err());
}
#[test] #[test]
fn bold_text() { fn bold_text() {
@@ -128,4 +141,43 @@ mod test {
] ]
); );
} }
#[test]
fn single_hyperlink() {
let md = "a link to [my site](https://example.com)";
let inl = parse_inlines(md).unwrap();
assert_eq!(
inl,
vec![
Inline::Text("a link to ".to_string()),
Inline::Link {
text: vec![Inline::Text("my site".to_string())],
href: "https://example.com".to_string()
}
]
);
}
#[test]
fn hyperlink_without_link() {
let md = "[abc]";
let inl = parse_inlines(md);
assert!(inl.is_err());
}
#[test]
fn escape_brackets() {
let md = r"some \[text\]";
let inl = parse_inlines(md).unwrap();
assert_eq!(inl, vec![Inline::Text("some [text]".to_string())]);
}
#[test]
fn escape_escape() {
let md = r"backslash \\";
let inl = parse_inlines(md).unwrap();
assert_eq!(inl, vec![Inline::Text(r"backslash \".to_string())]);
}
} }

View File

@@ -1,5 +1,5 @@
{ {
description = "stdsrv Flake file"; description = "gravel project flake";
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable"; nixpkgs.url = "nixpkgs/nixos-unstable";
@@ -14,16 +14,18 @@
flake-utlis.lib.eachDefaultSystem ( flake-utlis.lib.eachDefaultSystem (
system: let system: let
pkgs = import nixpkgs {inherit system;}; pkgs = import nixpkgs {inherit system;};
stdsrv = import ./package.nix {inherit pkgs;}; gravel = import ./package.nix {inherit pkgs;};
in { in {
packages.default = stdsrv; packages.default = gravel;
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
packages = [ packages = with pkgs; [
pkgs.rustup rustup
pkgs.bacon helix
pkgs.cargo-nextest bacon
pkgs.cargo-expand cargo-nextest
pkgs.cargo-watch cargo-expand
cargo-watch
cargo-tarpaulin
]; ];
}; };
} }

14
gravel_cli/Cargo.toml Normal file
View File

@@ -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" }

173
gravel_cli/src/config.rs Normal file
View File

@@ -0,0 +1,173 @@
//! Simple and program specific command line argument parsing solution.
// todo: refactor to <command> <subcommand> [<options>]
use slogger::{LOG_LEVEL, Level, log};
use crate::error::Error;
use std::env::Args;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
pub enum Command {
Generate { force: bool, single: bool },
Serve { addr: Ipv4Addr, port: u16 },
Init,
}
impl Default for Command {
fn default() -> Self {
Self::Generate {
force: true,
single: false,
}
}
}
impl TryFrom<Args> for Command {
type Error = Error;
fn try_from(mut value: Args) -> Result<Self, Self::Error> {
let mut comm = Command::default();
let _ = value.next(); // ignore executable
let command = value.next();
// `gravel serve` command
if let Some("serve") = command.as_deref() {
let mut addr = Ipv4Addr::UNSPECIFIED;
let mut port = 8080;
while let Some(a) = value.next() {
match a.as_str() {
"-a" => {
let address_str = value.next().ok_or(Error::CommandLineArgsParse(
"Missing argument after `-a`. Expected IPv4 address.".to_string(),
))?;
addr = Ipv4Addr::parse_ascii(address_str.as_bytes()).map_err(|_e| {
Error::CommandLineArgsParse("Parsing IP address failed".to_string())
})?;
}
"-p" => {
let port_str = value.next().ok_or(Error::CommandLineArgsParse(
"Missing argument after `-p`. Expected TCP port number.".to_string(),
))?;
port = port_str.parse().map_err(|_e| {
Error::CommandLineArgsParse("Parsing TCP port failed".to_string())
})?;
}
&_ => Err(Error::CommandLineArgsParse(format!(
"Unknown argument: `{a}`"
)))?,
}
}
comm = Command::Serve { addr, port };
}
// `gravel init` command
else if let Some("init") = command.as_deref() {
if let Some(a) = value.next() {
Err(Error::CommandLineArgsParse(format!(
"Unexpected argument: `{a}`"
)))?;
}
comm = Command::Init;
}
// `gravel` command
for a in value {
match a.as_str() {
"-s" => {
comm = Command::Generate {
force: true,
single: true,
}
}
_ => Err(Error::CommandLineArgsParse(format!(
"Unknown argument: `{a}`"
)))?,
}
}
Ok(comm)
}
}
#[allow(unused)]
pub struct ProgramConfig {
pub outdir: PathBuf,
pub indir: PathBuf,
pub command: Command,
pub verbose: bool,
}
impl Default for ProgramConfig {
fn default() -> Self {
Self {
indir: PathBuf::from("./pebbles"),
outdir: PathBuf::from("./site"),
command: Command::default(),
verbose: true,
}
}
}
impl ProgramConfig {
pub fn new<P: AsRef<Path>>(_toml_file: P, args: Args) -> Result<Self, Error> {
let conf = Self {
command: args.try_into()?,
..Default::default()
};
LOG_LEVEL.get_or_init(|| Level::Debug);
log!(Level::Warn, "TOML parsing not implemented, skipping");
Ok(conf)
}
}
/*
impl TryFrom<std::env::Args> for ProgramConfig {
type Error = crate::error::Error;
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
let mut a = Self::default();
let _ = value.next(); // ignore executable path
while let Some(v) = value.next() {
match v.as_str() {
"-i" => {
a.indir = value
.next()
.ok_or(Error::CommandLineArgsParse(
"Expected input directory after option `-i`".to_string(),
))?
.into();
}
"-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;
LOG_LEVEL.get_or_init(|| Level::Debug);
}
_ => {
a.outdir = v.into();
}
}
}
LOG_LEVEL.get_or_init(|| Level::Info);
Ok(a)
}
}
*/

32
gravel_cli/src/error.rs Normal file
View File

@@ -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<cracked_md::Error> for Error {
fn from(value: cracked_md::Error) -> Self {
Self::MdParse(value)
}
}
impl From<stdsrv::error::Error> for Error {
fn from(value: stdsrv::error::Error) -> Self {
Self::Server(value)
}
}

37
gravel_cli/src/main.rs Normal file
View File

@@ -0,0 +1,37 @@
#![feature(addr_parse_ascii, never_type)]
use std::process;
use config::{Command, ProgramConfig};
use cracked_md::generate;
use error::Error;
use slogger::{Level, log};
use stdsrv::serve;
mod config;
mod error;
fn run() -> Result<(), Error> {
let conf = ProgramConfig::new("gravel.toml", std::env::args())?;
match conf.command {
Command::Init => todo!("project init"),
Command::Serve { addr, port } => serve(addr, port, conf.outdir)?,
Command::Generate {
force,
single: false,
} => generate(&conf.indir, &conf.outdir, force)?,
Command::Generate {
force: _f,
single: true,
} => todo!("single file generation"),
}
Ok(())
}
fn main() {
let _ = run().map_err(|e| {
log!(Level::Error, "{}", e);
process::exit(1);
});
}

View File

@@ -5,5 +5,6 @@ targets = [
] ]
components = [ components = [
"clippy", "clippy",
"rustfmt" "rustfmt",
"rust-analyzer"
] ]

6
slogger/Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "slogger"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -1,7 +1,9 @@
use std::fmt::Display; use std::{fmt::Display, sync::OnceLock};
pub static LOG_LEVEL: OnceLock<Level> = OnceLock::new();
#[derive(Debug)]
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level { pub enum Level {
Error, Error,
Warn, Warn,
@@ -25,18 +27,23 @@ impl Display for Level {
} }
/// A logging macro. Takes a [`Level`] and a formatted string. /// A logging macro. Takes a [`Level`] and a formatted string.
///
/// [`Level`]: ./logger/enum.Level.html
#[macro_export] #[macro_export]
macro_rules! log { macro_rules! log {
($level:expr, $($arg:tt)*) => {{ ($level:expr, $($arg:tt)*) => {{
let log_from = if &$level == &$crate::Level::Debug {
format!(" {}:{}", std::file!(), std::line!())
} else {
String::new()
};
if &$level <= $crate::LOG_LEVEL.get().unwrap_or(&$crate::Level::Info) {
println!( println!(
"{} {}:{}:{}: {}", "{}{}: {}",
$level, $level,
std::module_path!(), log_from,
std::file!(),
std::line!(),
format!($($arg)*) format!($($arg)*)
); );
}
}}; }};
} }
// todo: implement clean verbose/short logging

View File

@@ -3,11 +3,7 @@ name = "stdsrv"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]]
name = "gravel"
path = "src/main.rs"
# local dependencies # local dependencies
[dependencies] [dependencies]
cracked_md = { path = "../cracked_md" }
fstools = { path = "../fstools" } fstools = { path = "../fstools" }
slogger = { path = "../slogger" }

View File

@@ -1,60 +0,0 @@
//! Simple and program specific command line argument parsing solution.
use std::path::PathBuf;
use crate::error::Error;
use crate::error::ErrorKind;
pub struct ProgramArgs {
pub outdir: PathBuf,
pub indir: PathBuf,
pub generate: bool,
pub force: bool,
pub addr: String,
}
impl Default for ProgramArgs {
fn default() -> Self {
Self {
indir: PathBuf::from("./web"),
outdir: PathBuf::from("./html"),
generate: false,
force: false,
addr: "0.0.0.0:8080".to_string(),
}
}
}
impl TryFrom<std::env::Args> for ProgramArgs {
type Error = crate::error::Error;
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
let mut a = Self::default();
let _ = value.next(); // ignore executable path
while let Some(v) = value.next() {
match v.as_str() {
"-i" => {
a.indir = value
.next()
.ok_or(Error::new(
ErrorKind::CommandLineArgsParse,
"Expected input directory after option `-i`",
))?
.into();
}
"-a" => {
a.addr = value.next().ok_or(Error::new(
ErrorKind::CommandLineArgsParse,
"Expected listener address after option `-a`",
))?;
}
"-g" => a.generate = true,
"-f" => a.force = true,
_ => {
a.outdir = v.into();
}
}
}
Ok(a)
}
}

View File

@@ -27,6 +27,7 @@ pub struct Error {
} }
impl Error { impl Error {
#[must_use]
pub fn new(kind: ErrorKind, msg: &str) -> Self { pub fn new(kind: ErrorKind, msg: &str) -> Self {
Self { Self {
kind, kind,

View File

@@ -1,8 +1,8 @@
//! A simple server implementation that just responds with the contents of the file requested in //! A simple server implementation that just responds with the contents of the file requested in
//! the provided directory. //! the provided directory.
use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, path::Path};
use crate::{ use crate::{
error::{Error, ErrorKind, Result}, error::{Error, ErrorKind, Result},
@@ -16,7 +16,7 @@ pub struct FileServer {
} }
impl FileServer { impl FileServer {
pub fn new(root: &PathBuf) -> Result<FileServer> { pub fn new(root: &Path) -> Result<FileServer> {
if !root.is_dir() { if !root.is_dir() {
return Err(Error::new( return Err(Error::new(
ErrorKind::DirNotFound, ErrorKind::DirNotFound,

View File

@@ -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<Self::Item> {
// 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,
))
}
}

View File

@@ -1,31 +1,36 @@
//! A simple web server with 0 dependencies (other than Rust's stdlib). //! 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::{ use std::{
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
net::TcpListener, net::{Ipv4Addr, TcpListener},
process, path::PathBuf,
}; };
use args::ProgramArgs; use error::Error;
use cracked_md::generate;
use fileserver::FileServer; use fileserver::FileServer;
use logger::Level;
use request::HttpRequest; use request::HttpRequest;
use responder::Responder; use responder::Responder;
use slogger::{Level, log};
mod args; pub mod error;
mod error;
mod fileserver; mod fileserver;
mod http_header; mod http_header;
//mod http_stream;
mod logger;
mod request; mod request;
mod responder; mod responder;
mod response; mod response;
/// Entrypoint to the program. /// Opens a file server on a specified address and port which serves all files in dir.
fn main() -> Result<(), Box<dyn std::error::Error>> { ///
/// # 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<(), Error> {
/*
let args: ProgramArgs = std::env::args().try_into()?; let args: ProgramArgs = std::env::args().try_into()?;
if args.generate { if args.generate {
@@ -51,18 +56,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
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() { for stream in listener.incoming() {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
let outdir = args.outdir.clone(); let outdir = dir.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
log!(Level::Debug, "TcpStream handler spawned"); log!(Level::Debug, "TcpStream handler spawned");
let mut reader = BufReader::new(&stream); let mut reader = BufReader::new(&stream);
let mut writer = BufWriter::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, Ok(s) => s,
Err(_e) => return, Err(_e) => return,
}; };
@@ -81,6 +88,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(e) => log!(Level::Warn, "Connection failed: {}", e), Err(e) => log!(Level::Warn, "Connection failed: {}", e),
} }
} }
Ok(()) Ok(())
} }

View File

@@ -2,8 +2,7 @@
use crate::error::{Error, ErrorKind, Result}; use crate::error::{Error, ErrorKind, Result};
use crate::http_header::HttpHeaders; use crate::http_header::HttpHeaders;
use crate::log; use slogger::{Level, log};
use crate::logger::Level;
use std::fmt::Display; use std::fmt::Display;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::net::TcpStream; use std::net::TcpStream;
@@ -111,6 +110,14 @@ impl TryFrom<&mut BufReader<&TcpStream>> for HttpRequest {
} }
} }
log!(
Level::Info,
"{} /{} {}",
req.method,
req.path.display(),
req.version
);
Ok(req) Ok(req)
} }
} }

View File

@@ -2,8 +2,7 @@
use crate::error::Result; use crate::error::Result;
use crate::http_header::HttpHeaders; use crate::http_header::HttpHeaders;
use crate::log; use slogger::{Level, log};
use crate::logger::Level;
use std::{fmt::Display, io::Write}; use std::{fmt::Display, io::Write};
/// Macro for generating Http status codes (AI generated). /// Macro for generating Http status codes (AI generated).
@@ -119,7 +118,9 @@ impl HttpResponse {
let _ = std::io::Read::read(stream, &mut [0u8; 1]); let _ = std::io::Read::read(stream, &mut [0u8; 1]);
*/ */
log!(Level::Info, "\n{}", &self); // todo better verbose tracking
log!(Level::Info, "{} {}", self.version, self.status);
log!(Level::Debug, "\n{}", &self);
Ok(()) Ok(())
} }