//! see [RequestPath] use std::{fs::Metadata, ops::Deref}; use camino::{Utf8Path, Utf8PathBuf}; use crate::RuntimeError; /// The entrypoint into the filesystem /// /// This structure is used to process every path that comes from the web. /// When a route has a path, it passes it to [`RequestPath::verify_safe()`] /// and gets this structure back. #[derive(Clone, Debug, PartialEq)] pub struct RequestPath { request_path: String, system_path: Utf8PathBuf, is_dirfile: bool, } impl RequestPath { /// Use the `root` to verify a received `path` is safe to access and serve. /// This function will canonicalize it and return a `RequestPath` if the file /// exists and is within the root directory. pub fn verify_safe(root: &Utf8Path, path: &str) -> Result { let ended_in_slash = path.ends_with('/'); let system_unsafe_path = root.join(path); match system_unsafe_path.canonicalize_utf8() { Ok(canon) => match canon.strip_prefix(root) { Err(_e) => Err(RuntimeError::outside_root(path.to_owned(), canon)), Ok(path) => { let request_path = match ended_in_slash { true => format!("/{path}/"), false => format!("/{path}"), }; let (system_path, is_dirfile) = Self::file_or_dirfile(canon)?; Ok(RequestPath { request_path, system_path, is_dirfile, }) } }, Err(_io) => Err(RuntimeError::not_found( path.to_string(), system_unsafe_path, )), } } fn file_or_dirfile(path: Utf8PathBuf) -> Result<(Utf8PathBuf, bool), RuntimeError> { let meta = path .metadata() .map_err(|e| RuntimeError::file_error(e, path.clone()))?; if meta.is_dir() { let name = path.file_stem().unwrap(); let dirfile = path.join(format!("{name}.html")); if dirfile.exists() { return Ok((dirfile, true)); } } Ok((path, false)) } /// The web path from the request, after canonicalization. pub fn web_path(&self) -> &str { &self.request_path } /// A verified-safe system path. pub fn system_path<'p>(&'p self) -> SafePath<'p> { SafePath(self.system_path.as_path()) } /// Returns whether or not this path is a dirfile pub fn is_dirfile(&self) -> bool { self.is_dirfile } /// Guesses a files [`Mime`] from it's extension. /// /// If it cannot guess, a [`Mime`] with an empty string with the /// [`MimeType::Media`] is returned pub fn guess_mime(&self) -> Mime { let stem = self.system_path.file_stem().unwrap_or_default(); let ext = self.system_path.extension().unwrap_or_default(); match ext { // Text "css" => "text/css".as_mime(Mimetype::Text), "html" => "text/html".as_mime(Mimetype::Text), "js" => "text/javascript".as_mime(Mimetype::Text), "txt" => "text/plain".as_mime(Mimetype::Text), "xml" if stem.ends_with("atom") => "application/atom+xml".as_mime(Mimetype::Text), "xml" => "application/xml".as_mime(Mimetype::Text), // Multimedia "gif" => "image/gif".as_mime(Mimetype::Media), "jpg" | "jpeg" => "image/jpeg".as_mime(Mimetype::Media), "mp4" => "video/mp4".as_mime(Mimetype::Media), "png" => "image/png".as_mime(Mimetype::Media), _ => "".as_mime(Mimetype::Media), } } } /// A verified-safe system path. #[derive(Copy, Clone, Debug, PartialEq)] pub struct SafePath<'p>(&'p Utf8Path); impl<'p> SafePath<'p> { pub fn to_owned(&self) -> SafePathBuf { SafePathBuf(self.0.to_path_buf()) } pub fn metadata(&self) -> Result { self.0 .metadata() .map_err(|err| RuntimeError::file_error(err, self.0.to_path_buf())) } } impl<'p> Deref for SafePath<'p> { type Target = &'p Utf8Path; fn deref(&self) -> &Self::Target { &self.0 } } impl<'p> AsRef for SafePath<'p> { fn as_ref(&self) -> &Utf8Path { self.0 } } /// Owned verified-safe system path. #[derive(Clone, Debug, PartialEq)] pub struct SafePathBuf(Utf8PathBuf); impl SafePathBuf { pub fn as_path<'p>(&'p self) -> SafePath<'p> { SafePath(&self.0) } } impl Deref for SafePathBuf { type Target = Utf8Path; fn deref(&self) -> &Self::Target { &self.0 } } impl AsRef for SafePathBuf { fn as_ref(&self) -> &Utf8PathBuf { &self.0 } } /// Mime string /// /// A mime type string, `"text/html"` or `"image/gif"`, and a [`MimeType`] that /// indicates what *kind* of file it is, so we know how to read it. /// /// See also: [`AsMime`] pub struct Mime { string: &'static str, mimetype: Mimetype, } impl Mime { pub fn mimetype(&self) -> Mimetype { self.mimetype } } impl AsRef for Mime { fn as_ref(&self) -> &str { self.string } } /// Convenience trait to turn types into [`Mime`] pub trait AsMime { fn as_mime(self, mimetype: Mimetype) -> Mime; } impl AsMime for &'static str { fn as_mime(self, mimetype: Mimetype) -> Mime { Mime { string: self, mimetype, } } } /// A very high-level description of a file #[derive(Copy, Clone, Debug, PartialEq)] pub enum Mimetype { /// Any text. HTML, Plainext, etc. Text, /// Any non-string based file. MP4, GIF, etc. Media, }