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") ); } }