eggserve-core 0.1.1

Security policy, path confinement, and static-serving primitives for eggserve
Documentation
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathRejection {
    Empty,
    #[allow(dead_code)]
    TooLong,
    UnsupportedUriForm,
    MalformedPercentEncoding,
    InvalidUtf8,
    NulByte,
    #[allow(dead_code)]
    AbsolutePath,
    ParentComponent,
    CurrentComponent,
    SeparatorAmbiguity,
    DotfileDenied,
    WindowsPrefixDenied,
    WindowsReservedNameDenied,
    WindowsAlternateStreamDenied,
    SymlinkDenied,
    RootEscapeDenied,
}

impl fmt::Display for PathRejection {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PathRejection::Empty => write!(f, "empty path"),
            PathRejection::TooLong => write!(f, "path too long"),
            PathRejection::UnsupportedUriForm => write!(f, "unsupported URI form"),
            PathRejection::MalformedPercentEncoding => {
                write!(f, "malformed percent encoding")
            }
            PathRejection::InvalidUtf8 => write!(f, "invalid UTF-8"),
            PathRejection::NulByte => write!(f, "NUL byte in path"),
            PathRejection::AbsolutePath => write!(f, "absolute path"),
            PathRejection::ParentComponent => write!(f, "parent component (..)"),
            PathRejection::CurrentComponent => write!(f, "current component (.)"),
            PathRejection::SeparatorAmbiguity => write!(f, "separator ambiguity"),
            PathRejection::DotfileDenied => write!(f, "dotfile denied"),
            PathRejection::WindowsPrefixDenied => write!(f, "Windows prefix denied"),
            PathRejection::WindowsReservedNameDenied => {
                write!(f, "Windows reserved name denied")
            }
            PathRejection::WindowsAlternateStreamDenied => {
                write!(f, "Windows alternate stream denied")
            }
            PathRejection::SymlinkDenied => write!(f, "symlink denied by policy"),
            PathRejection::RootEscapeDenied => write!(f, "canonical path escapes root"),
        }
    }
}

impl std::error::Error for PathRejection {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_all_variants() {
        let cases = &[
            (PathRejection::Empty, "empty path"),
            (PathRejection::TooLong, "path too long"),
            (PathRejection::UnsupportedUriForm, "unsupported URI form"),
            (
                PathRejection::MalformedPercentEncoding,
                "malformed percent encoding",
            ),
            (PathRejection::InvalidUtf8, "invalid UTF-8"),
            (PathRejection::NulByte, "NUL byte in path"),
            (PathRejection::AbsolutePath, "absolute path"),
            (PathRejection::ParentComponent, "parent component (..)"),
            (PathRejection::CurrentComponent, "current component (.)"),
            (PathRejection::SeparatorAmbiguity, "separator ambiguity"),
            (PathRejection::DotfileDenied, "dotfile denied"),
            (PathRejection::WindowsPrefixDenied, "Windows prefix denied"),
            (
                PathRejection::WindowsReservedNameDenied,
                "Windows reserved name denied",
            ),
            (
                PathRejection::WindowsAlternateStreamDenied,
                "Windows alternate stream denied",
            ),
            (PathRejection::SymlinkDenied, "symlink denied by policy"),
            (
                PathRejection::RootEscapeDenied,
                "canonical path escapes root",
            ),
        ];
        for (rejection, expected) in cases {
            assert_eq!(rejection.to_string(), *expected);
        }
    }

    #[test]
    fn is_error() {
        let err: &dyn std::error::Error = &PathRejection::Empty;
        assert_eq!(err.to_string(), "empty path");
    }
}