kcode-rust-source 0.1.0

Validate complete reusable UTF-8 Rust source snapshots
Documentation
//! Neutral validation for complete UTF-8 Rust source snapshots.

use std::collections::BTreeMap;
use std::error::Error as StdError;
use std::fmt;

/// One complete UTF-8 source file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct File {
    /// Canonical slash-separated path relative to the package root.
    pub path: String,
    /// Complete UTF-8 file contents.
    pub contents: String,
}

/// A validated, canonically ordered complete Rust source snapshot.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Source {
    files: Vec<File>,
    name: String,
    version: String,
}

/// A source-validation failure.
pub struct Error {
    category: &'static str,
    message: String,
}

/// Result type returned by this crate.
pub type Result<T> = std::result::Result<T, Error>;

impl Error {
    fn new(category: &'static str, message: impl Into<String>) -> Self {
        Self {
            category,
            message: message.into(),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}: {}", self.category, self.message)
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Error")
            .field("category", &self.category)
            .field("message", &self.message)
            .finish()
    }
}

impl StdError for Error {}

/// Validates a managed package name.
pub fn validate_name(name: &str) -> Result<()> {
    let mut bytes = name.bytes();
    if !bytes
        .next()
        .is_some_and(|byte| byte.is_ascii_alphanumeric())
        || !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
    {
        return Err(Error::new(
            "invalid_name",
            format!("invalid managed-package name {name:?}"),
        ));
    }
    Ok(())
}

impl Source {
    /// Validates and canonically orders a complete source snapshot.
    pub fn validate(files: &[File], expected_name: &str) -> Result<Self> {
        validate_name(expected_name)?;
        let mut ordered = BTreeMap::new();
        for file in files {
            validate_path(&file.path)?;
            if file.path == "Cargo.lock" {
                return Err(Error::new(
                    "invalid_source",
                    "Cargo.lock is ephemeral and cannot be managed source",
                ));
            }
            if ordered
                .insert(file.path.clone(), file.contents.clone())
                .is_some()
            {
                return Err(Error::new(
                    "invalid_source",
                    format!("duplicate source path {:?}", file.path),
                ));
            }
        }

        let manifest = ordered
            .get("Cargo.toml")
            .ok_or_else(|| Error::new("invalid_source", "root Cargo.toml is required"))?;
        if !ordered.contains_key("Documentation.md") {
            return Err(Error::new(
                "invalid_source",
                "root Documentation.md is required",
            ));
        }
        let (name, version) = manifest_metadata(manifest)?;
        if name != expected_name {
            return Err(Error::new(
                "invalid_metadata",
                format!("[package].name must be {expected_name:?}, found {name:?}"),
            ));
        }

        Ok(Self {
            files: ordered
                .into_iter()
                .map(|(path, contents)| File { path, contents })
                .collect(),
            name,
            version,
        })
    }

    /// Returns all files in canonical path order.
    pub fn files(&self) -> &[File] {
        &self.files
    }

    /// Returns the literal root manifest package name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the canonical stable root manifest package version.
    pub fn version(&self) -> &str {
        &self.version
    }
}

fn manifest_metadata(manifest: &str) -> Result<(String, String)> {
    let mut section = String::new();
    let mut name = None;
    let mut version = None;

    for raw_line in manifest.lines() {
        let line = strip_comment(raw_line).trim();
        if line.is_empty() {
            continue;
        }
        if line.starts_with('[') && line.ends_with(']') {
            section.clear();
            section.push_str(line[1..line.len() - 1].trim());
            continue;
        }
        if section != "package" {
            continue;
        }
        let Some((raw_key, raw_value)) = line.split_once('=') else {
            continue;
        };
        match raw_key.trim() {
            "name" => {
                if name.is_some() {
                    return Err(Error::new("invalid_metadata", "duplicate [package].name"));
                }
                name = Some(parse_basic_string(raw_value.trim(), "name")?);
            }
            "version" => {
                if version.is_some() {
                    return Err(Error::new(
                        "invalid_metadata",
                        "duplicate [package].version",
                    ));
                }
                version = Some(parse_basic_string(raw_value.trim(), "version")?);
            }
            _ => {}
        }
    }

    let name = name.ok_or_else(|| {
        Error::new(
            "invalid_metadata",
            "literal [package].name is required in root Cargo.toml",
        )
    })?;
    validate_name(&name)?;
    let version = version.ok_or_else(|| {
        Error::new(
            "invalid_metadata",
            "literal [package].version is required in root Cargo.toml",
        )
    })?;
    validate_version(&version)?;
    Ok((name, version))
}

fn validate_path(path: &str) -> Result<()> {
    if path.is_empty()
        || path.starts_with('/')
        || path.ends_with('/')
        || path.contains('\\')
        || path.contains(':')
        || path.contains('\0')
        || path
            .split('/')
            .any(|component| component.is_empty() || matches!(component, "." | ".."))
    {
        return Err(Error::new(
            "unsafe_path",
            format!("invalid relative source path {path:?}"),
        ));
    }
    Ok(())
}

fn validate_version(version: &str) -> Result<()> {
    let components = version.split('.').collect::<Vec<_>>();
    if components.len() != 3
        || components.iter().any(|component| {
            component.is_empty()
                || !component.bytes().all(|byte| byte.is_ascii_digit())
                || (component.len() > 1 && component.starts_with('0'))
                || component.parse::<u64>().is_err()
        })
    {
        return Err(Error::new(
            "invalid_metadata",
            format!("noncanonical stable version {version:?}"),
        ));
    }
    Ok(())
}

fn parse_basic_string(value: &str, field: &str) -> Result<String> {
    if value.len() < 2 || !value.starts_with('"') || !value.ends_with('"') {
        return Err(Error::new(
            "invalid_metadata",
            format!("[package].{field} must be a literal basic string"),
        ));
    }
    let inner = &value[1..value.len() - 1];
    if inner.contains(['"', '\\', '\n', '\r']) {
        return Err(Error::new(
            "invalid_metadata",
            format!("[package].{field} must not contain escapes or newlines"),
        ));
    }
    Ok(inner.to_owned())
}

fn strip_comment(line: &str) -> &str {
    let mut quoted = false;
    let mut escaped = false;
    for (index, character) in line.char_indices() {
        if escaped {
            escaped = false;
        } else if character == '\\' && quoted {
            escaped = true;
        } else if character == '"' {
            quoted = !quoted;
        } else if character == '#' && !quoted {
            return &line[..index];
        }
    }
    line
}

#[cfg(test)]
mod tests {
    use super::{File, Source};

    fn files(version: &str) -> Vec<File> {
        vec![
            File {
                path: "src/lib.rs".to_owned(),
                contents: String::new(),
            },
            File {
                path: "Documentation.md".to_owned(),
                contents: "API\n".to_owned(),
            },
            File {
                path: "Cargo.toml".to_owned(),
                contents: format!(
                    "[workspace]\nresolver = \"3\"\n\n[package]\nname = \"demo\"\nversion = \"{version}\" # current\n"
                ),
            },
        ]
    }

    #[test]
    fn validates_metadata_and_canonicalizes_complete_source() {
        let source = Source::validate(&files("12.3.4"), "demo").unwrap();
        assert_eq!(source.name(), "demo");
        assert_eq!(source.version(), "12.3.4");
        assert_eq!(
            source
                .files()
                .iter()
                .map(|file| file.path.as_str())
                .collect::<Vec<_>>(),
            ["Cargo.toml", "Documentation.md", "src/lib.rs"]
        );
    }

    #[test]
    fn rejects_noncanonical_versions_unsafe_paths_and_lockfiles() {
        for version in [
            "1.2",
            "01.2.3",
            "1.2.3-beta",
            "1.2.3+build",
            "123456789012345678901234567890.2.3",
        ] {
            assert!(Source::validate(&files(version), "demo").is_err());
        }

        let mut unsafe_files = files("1.2.3");
        unsafe_files.push(File {
            path: "../escape".to_owned(),
            contents: String::new(),
        });
        assert!(Source::validate(&unsafe_files, "demo").is_err());

        let mut locked_files = files("1.2.3");
        locked_files.push(File {
            path: "Cargo.lock".to_owned(),
            contents: "version = 4\n".to_owned(),
        });
        assert!(Source::validate(&locked_files, "demo").is_err());
    }

    #[test]
    fn requires_unique_files_docs_and_matching_literal_name() {
        let mut duplicate = files("1.2.3");
        duplicate.push(duplicate[0].clone());
        assert!(Source::validate(&duplicate, "demo").is_err());

        let mut no_docs = files("1.2.3");
        no_docs.retain(|file| file.path != "Documentation.md");
        assert!(Source::validate(&no_docs, "demo").is_err());

        let inherited = "[package]\nname.workspace = true\nversion = \"1.2.3\"\n".to_owned();
        let mut invalid = files("1.2.3");
        invalid[2].contents = inherited;
        assert!(Source::validate(&invalid, "demo").is_err());
    }
}