kcode-rust-libs 0.2.3

Manage, validate, document, and publish small agent-authored Rust crates
Documentation
use std::fmt;
use std::str::FromStr;

use crate::{Error, Result};

/// A canonical, UTF-8 path to a file relative to a managed Rust library root.
///
/// The serialized/displayed form always uses `/` separators.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RustLibPath(String);

impl RustLibPath {
    /// Validate and construct a library-relative path.
    pub fn new(path: impl Into<String>) -> Result<Self> {
        let path = path.into();
        validate(&path)?;
        Ok(Self(path))
    }

    /// Return the canonical string representation.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Consume the path and return its string representation.
    pub fn into_string(self) -> String {
        self.0
    }
}

impl AsRef<str> for RustLibPath {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

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

impl FromStr for RustLibPath {
    type Err = Error;

    fn from_str(value: &str) -> Result<Self> {
        Self::new(value)
    }
}

impl TryFrom<String> for RustLibPath {
    type Error = Error;

    fn try_from(value: String) -> Result<Self> {
        Self::new(value)
    }
}

impl TryFrom<&str> for RustLibPath {
    type Error = Error;

    fn try_from(value: &str) -> Result<Self> {
        Self::new(value)
    }
}

fn validate(path: &str) -> Result<()> {
    let invalid = |reason| Error::InvalidRustLibPath {
        path: path.to_owned(),
        reason,
    };

    if path.is_empty() {
        return Err(invalid("path is empty"));
    }
    if path.starts_with('/') {
        return Err(invalid("absolute paths are not allowed"));
    }
    if path.ends_with('/') {
        return Err(invalid("trailing '/' is not allowed"));
    }
    if path.contains('\\') {
        return Err(invalid("use '/' rather than backslash as the separator"));
    }
    if path.contains(':') {
        return Err(invalid("':' is not allowed in Rust library paths"));
    }
    if path.contains('\0') {
        return Err(invalid("NUL is not allowed"));
    }

    for component in path.split('/') {
        if component.is_empty() {
            return Err(invalid("empty path components are not allowed"));
        }
        if component == "." || component == ".." {
            return Err(invalid("'.' and '..' components are not allowed"));
        }
    }

    Ok(())
}

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

    #[test]
    fn accepts_canonical_relative_paths() {
        for path in ["Cargo.toml", "src/lib.rs", ".gitignore", "资料/代码.rs"] {
            assert!(RustLibPath::new(path).is_ok(), "{path}");
        }
    }

    #[test]
    fn rejects_unsafe_or_noncanonical_paths() {
        for path in [
            "",
            "/etc/passwd",
            "../outside",
            "src/../outside",
            "./src/lib.rs",
            "src//lib.rs",
            "src/lib.rs/",
            "src\\lib.rs",
            "C:drive-relative.rs",
            "nul\0name",
        ] {
            assert!(RustLibPath::new(path).is_err(), "{path:?}");
        }
    }
}