From c4eff133ed2a2c3deb8fad322a430b3263b6e6ab Mon Sep 17 00:00:00 2001 From: gennyble Date: Wed, 21 Feb 2024 05:40:24 -0600 Subject: initial commit; Filesytem and dirfiles --- src/fs.rs | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 + 2 files changed, 209 insertions(+) create mode 100644 src/fs.rs create mode 100644 src/main.rs (limited to 'src') diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..e13f405 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,206 @@ +use camino::{Utf8Path, Utf8PathBuf}; +use std::{io, ops::Deref, str::FromStr}; + +/// Webpath is the path we get from HTTP requests. It's garunteed to not fall +/// below the webroot and will never start or end with a slash. +#[derive(Clone, Debug, PartialEq)] +pub struct Webpath { + webcanon: Utf8PathBuf, +} + +impl FromStr for Webpath { + type Err = RuntimeError; + + fn from_str(raw: &str) -> Result { + let mut curr = Utf8PathBuf::new(); + + for component in raw.split('/') { + match component { + "." => continue, + ".." => { + if !curr.pop() { + return Err(RuntimeError::PathTooLow { + path: raw.to_owned(), + }); + } + } + comp => curr.push(comp), + } + } + + Ok(Self { webcanon: curr }) + } +} + +impl Webpath { + /// Return whether or not this Webpath is empty which would indicate it's + /// the homepage. + pub fn is_index(&self) -> bool { + self.webcanon == "" + } +} + +impl Deref for Webpath { + type Target = Utf8Path; + + fn deref(&self) -> &Self::Target { + &self.webcanon + } +} + +impl AsRef for Webpath { + fn as_ref(&self) -> &Utf8Path { + &self.webcanon + } +} + +impl PartialEq for Webpath { + fn eq(&self, other: &str) -> bool { + self.webcanon.eq(other) + } +} + +const ROOT_INDEX: &str = "home.html"; +const DIRFILE_EXT: &str = "html"; + +pub struct Filesystem { + webroot: Utf8PathBuf, +} + +impl Filesystem { + pub fn new>(root: P) -> Self { + Self { + webroot: root.into(), + } + } + + fn resolve(&self, webpath: &Webpath) -> Result { + if webpath.is_index() || webpath == ROOT_INDEX { + return Ok(self.webroot.join(ROOT_INDEX)); + } + + let path = self.webroot.join(webpath); + let metadata = Self::metadata(&path)?; + + // If it's a file then return the path immediatly + if metadata.is_file() { + Ok(path) + } else { + // we check this above. but still.. + //TODO: gen- probably don't unwrap + let filename = path.file_name().unwrap(); + let dirfile = path.join(format!("{filename}.{DIRFILE_EXT}")); + + if dirfile.exists() { + Ok(dirfile) + } else { + Err(RuntimeError::NotFound { + source: io::ErrorKind::NotFound.into(), + path: dirfile, + }) + } + } + } + + fn metadata>(path: P) -> Result { + path.as_ref() + .metadata() + .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) + } +} + +#[derive(Debug, snafu::Snafu)] +pub enum RuntimeError { + #[snafu(display("the path was not found: {path}"))] + NotFound { + source: io::Error, + path: Utf8PathBuf, + }, + #[snafu(display("io error: {path}: {source}"))] + UnknownIo { + source: io::Error, + path: Utf8PathBuf, + }, + #[snafu(display("path tried to go below webroot: {path}"))] + PathTooLow { path: String }, +} + +impl RuntimeError { + pub fn from_io(source: io::Error, path: Utf8PathBuf) -> Self { + match source.kind() { + io::ErrorKind::NotFound => RuntimeError::NotFound { source, path }, + _ => RuntimeError::UnknownIo { source, path }, + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::fs::{Webpath, ROOT_INDEX}; + + use super::Filesystem; + + macro_rules! webpath { + ($location:expr) => { + Webpath::from_str($location).unwrap() + }; + } + + #[test] + fn webpath_finds_too_low() { + assert!(Webpath::from_str("/..").is_err()); + assert!(Webpath::from_str("/one/..").is_ok()); + assert!(Webpath::from_str("/one/../..").is_err()); + assert!(Webpath::from_str("/one/../two").is_ok()) + } + + #[test] + fn webpath_path_correct() { + assert_eq!(Webpath::from_str("/one").unwrap().webcanon, "one"); + assert_eq!(Webpath::from_str("/one/..").unwrap().webcanon, ""); + assert_eq!(Webpath::from_str("/one/two/..").unwrap().webcanon, "one"); + assert_eq!(Webpath::from_str("/one/../two").unwrap().webcanon, "two"); + assert_eq!( + Webpath::from_str("/one/two/three/..").unwrap().webcanon, + "one/two" + ); + assert_eq!( + Webpath::from_str("/one/../home.html").unwrap().webcanon, + "home.html" + ); + } + + const TESTROOT: &str = "test/serve"; + + #[test] + fn filesystem_resolves_index() { + let fs = Filesystem::new(TESTROOT); + assert_eq!( + fs.resolve(&webpath!("/")).unwrap(), + format!("{TESTROOT}/{ROOT_INDEX}") + ); + assert_eq!( + fs.resolve(&webpath!("/one/..")).unwrap(), + format!("{TESTROOT}/{ROOT_INDEX}") + ); + } + + #[test] + fn filesystem_resolves_dirfile() { + let fs = Filesystem::new(TESTROOT); + assert_eq!( + fs.resolve(&webpath!("/one")).unwrap(), + format!("{TESTROOT}/one/one.html") + ); + assert_eq!( + fs.resolve(&webpath!("/one/eleven/")).unwrap(), + format!("{TESTROOT}/one/eleven/eleven.html") + ); + assert_eq!( + fs.resolve(&webpath!("/one/eleven/..")).unwrap(), + format!("{TESTROOT}/one/one.html") + ); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1ba94f7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +mod fs; + +fn main() {} -- cgit 1.4.1-3-g733a5