use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Source {
RelativePath(String),
}
impl Source {
pub fn relative_path(s: &str) -> anyhow::Result<Self> {
if s.is_empty() {
anyhow::bail!("source path cannot be empty");
}
if s.starts_with('/') {
anyhow::bail!("source path '{s}' must be relative, not absolute");
}
if s.split('/').any(|seg| seg == "..") {
anyhow::bail!("source path '{s}' must not contain '..'");
}
Ok(Self::RelativePath(s.to_owned()))
}
pub fn as_str(&self) -> &str {
match self {
Source::RelativePath(p) => p,
}
}
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
pub mod strategy {
use super::Source;
use proptest::prelude::*;
pub fn relative_path() -> impl Strategy<Value = Source> {
"[a-z][a-z0-9_-]{0,15}(/[a-z][a-z0-9_-]{0,15}){0,3}\\.[a-z]{1,4}"
.prop_map(|s| Source::relative_path(&s).unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_simple_filename() {
let s = Source::relative_path("index.md").unwrap();
assert_eq!(s.as_str(), "index.md");
}
#[test]
fn accepts_nested_path() {
let s = Source::relative_path("assets/css/site.css").unwrap();
assert_eq!(s.as_str(), "assets/css/site.css");
}
#[test]
fn accepts_filenames_with_spaces() {
let s = Source::relative_path("mockup UX.xml").unwrap();
assert_eq!(s.as_str(), "mockup UX.xml");
}
#[test]
fn rejects_empty() {
assert!(Source::relative_path("").is_err());
}
#[test]
fn rejects_absolute() {
assert!(Source::relative_path("/etc/passwd").is_err());
}
#[test]
fn rejects_parent_escape() {
assert!(Source::relative_path("../outside.md").is_err());
assert!(Source::relative_path("a/../../escape.md").is_err());
}
#[test]
fn display_renders_inner_path() {
let s = Source::relative_path("pages/intro.md").unwrap();
assert_eq!(format!("{s}"), "pages/intro.md");
}
proptest::proptest! {
#[test]
fn strategy_produces_valid_sources(s in strategy::relative_path()) {
let reparsed = Source::relative_path(s.as_str()).unwrap();
assert_eq!(reparsed, s);
}
}
}