vyuh 0.2.6

Vyuh web framework for Axum and SQLx with handler-first APIs
Documentation
use std::fmt;
use std::path::{Component, Path, PathBuf};

use super::FileStorageError;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StorageName(String);

impl StorageName {
    pub fn new(name: impl Into<String>) -> Result<Self, FileStorageError> {
        let name = name.into();
        validate_storage_name(&name)?;
        Ok(Self(name))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn join_to(&self, root: &Path) -> Result<PathBuf, FileStorageError> {
        let path = root.join(&self.0);
        let normalized_root = root.components().collect::<PathBuf>();
        let normalized_path = path.components().collect::<PathBuf>();
        if !normalized_path.starts_with(&normalized_root) {
            return Err(FileStorageError::EscapesRoot(path));
        }
        Ok(path)
    }
}

impl fmt::Display for StorageName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

fn validate_storage_name(name: &str) -> Result<(), FileStorageError> {
    if name.is_empty() {
        return Err(invalid(name, "cannot be empty"));
    }
    if name.contains('\0') {
        return Err(invalid(name, "cannot contain NUL bytes"));
    }
    if name.contains('\\') {
        return Err(invalid(name, "cannot contain backslashes"));
    }
    let path = Path::new(name);
    if path.is_absolute() {
        return Err(invalid(name, "must be relative"));
    }
    for component in path.components() {
        match component {
            Component::Normal(part) if !part.is_empty() => {}
            Component::CurDir => {}
            Component::ParentDir => return Err(invalid(name, "cannot contain '..'")),
            _ => return Err(invalid(name, "contains invalid path components")),
        }
    }
    Ok(())
}

fn invalid(name: &str, reason: &str) -> FileStorageError {
    FileStorageError::InvalidName {
        name: name.to_string(),
        reason: reason.to_string(),
    }
}