use std::{fmt, ops::Deref, str::FromStr}; use camino::{Utf8Path, Utf8PathBuf}; use crate::WikError; #[derive(Clone, Debug, PartialEq)] pub struct WikName { name: String, } impl WikName { /// Returns a new [WikName] without checking if it is valid pub fn new_unchecked(name: String) -> Self { Self { name } } pub fn as_path(&self) -> WikPath { self.into() } } #[derive(Clone, Debug, PartialEq)] pub struct WikPath { name: String, } impl WikPath { pub fn join(&self, name: &WikName) -> Self { Self { name: format!("{}/{name}", self.name), } } pub fn name(&self) -> WikName { let (_, name) = self.as_parts(); name } pub fn parent(&self) -> Option { self.name.rsplit_once('/').map(|(parent, _)| parent.into()) } pub fn as_parts(&self) -> (Option, WikName) { match self.name.rsplit_once('/') { None => (None, WikName::new_unchecked(self.name.clone())), Some((section, name)) => ( Some(section.into()), WikName::new_unchecked(name.to_owned()), ), } } } impl From for WikPath where T: Into, { fn from(value: T) -> Self { Self { name: value.into() } } } impl From<&WikName> for WikPath { fn from(name: &WikName) -> Self { WikPath { name: name.name.to_owned(), } } } impl FromStr for WikName { type Err = WikError; fn from_str(s: &str) -> Result { if s.contains('/') { Err(WikError::name_has_slashes(s)) } else { Ok(Self { name: s.to_owned() }) } } } impl AsRef for WikPath { fn as_ref(&self) -> &str { &self.name } } impl AsRef for WikName { fn as_ref(&self) -> &str { &self.name } } impl fmt::Display for WikPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } impl fmt::Display for WikName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } #[derive(Clone, Debug, PartialEq)] pub struct SafePath(pub Utf8PathBuf); impl Deref for SafePath { type Target = Utf8Path; fn deref(&self) -> &Self::Target { &self.0 } } impl AsRef for SafePath { fn as_ref(&self) -> &std::path::Path { self.0.as_std_path() } } impl fmt::Display for SafePath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Clone, Debug, PartialEq)] pub(crate) enum EitherPath { Wik(WikPath), Safe(SafePath), } impl EitherPath { /// Use when the file does not exist yet, but other components are expected to exist. pub fn safe_nonexist(root: &Utf8Path, wik: &WikPath) -> Result { let mut joined = root.join(&wik.name); let name = joined.file_name().map(<_>::to_owned); if !joined.pop() || name.is_none() { return Err(WikError::canonicalization_failed(joined)); } let name = name.unwrap(); let canon = joined .canonicalize_utf8() .map_err(|_| WikError::canonicalization_failed(joined))?; let path = canon.join(name); if !Self::check_fs_path(&path, root) { Err(WikError::illegal_path(wik.clone(), path)) } else { Ok(SafePath(path)) } } pub fn safe(self, root: &Utf8Path) -> Result { self.as_safe(root) } fn resolve_path(wik: &WikPath, root: &Utf8Path) -> Result { let fs_path = root.join(&wik.name); let fs_canon = fs_path .canonicalize_utf8() .map_err(|_| WikError::CanonicalizationFailed { path: fs_path })?; if !Self::check_fs_path(&fs_canon, root) { Err(WikError::illegal_path(wik.clone(), fs_canon)) } else { Ok(SafePath(fs_canon)) } } /// Returns `true` if the path is above the root, `false` otherwise fn check_fs_path(path: &Utf8Path, root: &Utf8Path) -> bool { path.starts_with(root) } fn as_safe(&self, root: &Utf8Path) -> Result { match self { Self::Wik(wik) => Self::resolve_path(wik, root), Self::Safe(safe) => Ok(safe.clone()), } } } impl From for EitherPath { fn from(value: WikPath) -> Self { Self::Wik(value) } } impl From for EitherPath { fn from(value: SafePath) -> Self { Self::Safe(value) } }