use serde::{Deserialize, Serialize}; use std::path::Component; use std::path::Path; use std::path::PathBuf; // Copypasta from . fn normalize(p: &Path) -> PathBuf { let mut stack: Vec = vec![]; // We assume .components() removes redundant consecutive path separators. // Note that .components() also does some normalization of '.' on its own anyways. // This '.' normalization happens to be compatible with the approach below. for component in p.components() { match component { // Drop CurDir components, do not even push onto the stack. Component::CurDir => {} // For ParentDir components, we need to use the contents of the stack. Component::ParentDir => { // Look at the top element of stack, if any. let top = stack.last().cloned(); match top { // A component is on the stack, need more pattern matching. Some(c) => { match c { // Push the ParentDir on the stack. Component::Prefix(_) => { stack.push(component); } // The parent of a RootDir is itself, so drop the ParentDir (no-op). Component::RootDir => {} // A CurDir should never be found on the stack, since they are dropped when seen. Component::CurDir => { unreachable!(); } // If a ParentDir is found, it must be due to it piling up at the start of a path. // Push the new ParentDir onto the stack. Component::ParentDir => { stack.push(component); } // If a Normal is found, pop it off. Component::Normal(_) => { let _ = stack.pop(); } } } // Stack is empty, so path is empty, just push. None => { stack.push(component); } } } // All others, simply push onto the stack. _ => { stack.push(component); } } } // If an empty PathBuf would be return, instead return CurDir ('.'). if stack.is_empty() { return PathBuf::from(Component::CurDir.as_os_str()); } let mut norm_path = PathBuf::new(); for item in &stack { norm_path.push(item.as_os_str()); } norm_path } #[derive(Debug, Clone)] pub struct Slug(pub String); impl Serialize for Slug { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.0.serialize(serializer) } } impl<'de> Deserialize<'de> for Slug { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; ensure_slug(&s).map_err(|e| serde::de::Error::custom(e))?; Ok(Self(s)) } } pub fn ensure_slug(text: &str) -> eyre::Result<()> { fn is_slug(s: &str) -> bool { !s.is_empty() && s.chars() .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || matches!(c, '-' | '_')) } eyre::ensure!(is_slug(text), "not a slug"); Ok(()) } #[derive(Debug, Clone)] pub struct FileName(pub String); impl<'de> Deserialize<'de> for FileName { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; let s = PathBuf::from(s); let res = normalize(&s); let res = res .file_name() .ok_or_else(|| serde::de::Error::custom("the fuck"))? .to_str() .ok_or_else(|| serde::de::Error::custom("nigger"))?; Ok(Self(res.to_owned())) } } impl Serialize for FileName { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.0.serialize(serializer) } }