kcode-rust-libs-v2 0.1.0

Manage, validate, and publish complete agent-authored Rust library snapshots
Documentation
use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use crate::{Error, Result, RustLibFile, RustLibPath};

pub(crate) struct RepositorySnapshot {
    pub version: String,
    pub files: Vec<RustLibFile>,
}

pub(crate) fn validate_name(name: &str) -> Result<()> {
    let mut characters = name.chars();
    let Some(first) = characters.next() else {
        return Err(Error::InvalidRustLibName(name.to_owned()));
    };
    if !first.is_ascii_alphanumeric()
        || !characters.all(|character| {
            character.is_ascii_alphanumeric() || character == '-' || character == '_'
        })
    {
        return Err(Error::InvalidRustLibName(name.to_owned()));
    }
    Ok(())
}

pub(crate) fn read_repository(root: &Path) -> Result<RepositorySnapshot> {
    let mut files = Vec::new();
    visit_directory(root, "", &mut files)?;
    files.sort_by(|left, right| left.path.cmp(&right.path));
    let version = validate_metadata(&files)?;
    Ok(RepositorySnapshot { version, files })
}

fn visit_directory(root: &Path, relative: &str, files: &mut Vec<RustLibFile>) -> Result<()> {
    let directory = if relative.is_empty() {
        root.to_path_buf()
    } else {
        root.join(relative)
    };
    let entries = fs::read_dir(&directory)
        .map_err(|source| Error::io("read directory", &directory, source))?;
    for entry in entries {
        let entry =
            entry.map_err(|source| Error::io("read directory entry", &directory, source))?;
        let name = entry
            .file_name()
            .into_string()
            .map_err(|name| Error::NonUtf8Path(PathBuf::from(name)))?;
        let child_relative = if relative.is_empty() {
            name
        } else {
            format!("{relative}/{name}")
        };
        let path = entry.path();
        let metadata = fs::symlink_metadata(&path)
            .map_err(|source| Error::io("inspect library entry", &path, source))?;
        if metadata.file_type().is_symlink() {
            return Err(Error::SymlinkNotAllowed(path));
        }
        if metadata.is_dir() {
            visit_directory(root, &child_relative, files)?;
        } else if metadata.is_file() {
            let bytes = fs::read(&path).map_err(|source| Error::io("read file", &path, source))?;
            let contents = String::from_utf8(bytes).map_err(|_| Error::NonUtf8File(path))?;
            files.push(RustLibFile::new(
                RustLibPath::new(child_relative)?,
                contents,
            ));
        } else {
            return Err(Error::UnsupportedFileType(path));
        }
    }
    Ok(())
}

pub(crate) fn write_repository(
    root: &Path,
    current: &[RustLibFile],
    writes: &[RustLibFile],
) -> Result<RepositorySnapshot> {
    let mut seen = BTreeSet::new();
    for file in writes {
        if !seen.insert(file.path.clone()) {
            return Err(Error::DuplicateWritePath(file.path.as_str().to_owned()));
        }
    }

    let mut projected = current.to_vec();
    for write in writes {
        if let Some(existing) = projected.iter_mut().find(|file| file.path == write.path) {
            existing.contents.clone_from(&write.contents);
        } else {
            projected.push(write.clone());
        }
    }
    projected.sort_by(|left, right| left.path.cmp(&right.path));
    validate_metadata(&projected)?;

    for write in writes {
        let destination = prepare_destination(root, &write.path)?;
        fs::write(&destination, write.contents.as_bytes())
            .map_err(|source| Error::io("write file", &destination, source))?;
    }
    read_repository(root)
}

fn prepare_destination(root: &Path, path: &RustLibPath) -> Result<PathBuf> {
    let components = path.as_str().split('/').collect::<Vec<_>>();
    let mut parent = root.to_path_buf();
    for component in &components[..components.len() - 1] {
        parent.push(component);
        match fs::symlink_metadata(&parent) {
            Ok(metadata) if metadata.file_type().is_symlink() => {
                return Err(Error::SymlinkNotAllowed(parent));
            }
            Ok(metadata) if metadata.is_dir() => {}
            Ok(_) => return Err(Error::UnsupportedFileType(parent)),
            Err(source) if source.kind() == io::ErrorKind::NotFound => {
                fs::create_dir(&parent)
                    .map_err(|source| Error::io("create parent directory", &parent, source))?;
            }
            Err(source) => return Err(Error::io("inspect parent directory", &parent, source)),
        }
    }

    let destination = root.join(path.as_str());
    match fs::symlink_metadata(&destination) {
        Ok(metadata) if metadata.file_type().is_symlink() => {
            Err(Error::SymlinkNotAllowed(destination))
        }
        Ok(metadata) if metadata.is_file() => Ok(destination),
        Ok(_) => Err(Error::UnsupportedFileType(destination)),
        Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(destination),
        Err(source) => Err(Error::io("inspect write destination", destination, source)),
    }
}

pub(crate) fn copy_repository(source: &Path, destination: &Path) -> Result<()> {
    fs::create_dir_all(destination)
        .map_err(|error| Error::io("create copied repository", destination, error))?;
    copy_directory(source, destination)
}

fn copy_directory(source: &Path, destination: &Path) -> Result<()> {
    let entries =
        fs::read_dir(source).map_err(|error| Error::io("read source directory", source, error))?;
    for entry in entries {
        let entry = entry.map_err(|error| Error::io("read source entry", source, error))?;
        let source_path = entry.path();
        let destination_path = destination.join(entry.file_name());
        let metadata = fs::symlink_metadata(&source_path)
            .map_err(|error| Error::io("inspect source entry", &source_path, error))?;
        if metadata.file_type().is_symlink() {
            return Err(Error::SymlinkNotAllowed(source_path));
        }
        if metadata.is_dir() {
            fs::create_dir(&destination_path)
                .map_err(|error| Error::io("create copied directory", &destination_path, error))?;
            copy_directory(&source_path, &destination_path)?;
        } else if metadata.is_file() {
            fs::copy(&source_path, &destination_path)
                .map_err(|error| Error::io("copy source file", &source_path, error))?;
        } else {
            return Err(Error::UnsupportedFileType(source_path));
        }
    }
    Ok(())
}

fn validate_metadata(files: &[RustLibFile]) -> Result<String> {
    let manifest = required_file(files, "Cargo.toml")?;
    required_file(files, "Documentation.md")?;
    parse_manifest_version(&manifest.contents)
}

fn required_file<'a>(files: &'a [RustLibFile], path: &'static str) -> Result<&'a RustLibFile> {
    files
        .iter()
        .find(|file| file.path.as_str() == path)
        .ok_or(Error::MissingRequiredFile(path))
}

fn parse_manifest_version(manifest: &str) -> Result<String> {
    let mut in_package = false;
    let mut found = None;
    for raw_line in manifest.lines() {
        let line = strip_comment(raw_line).trim();
        if line.is_empty() {
            continue;
        }
        if line.starts_with('[') {
            in_package = line == "[package]";
            continue;
        }
        if !in_package {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        if key.trim() != "version" {
            continue;
        }
        if found.is_some() {
            return Err(Error::InvalidCargoManifest(
                "[package].version is assigned more than once".to_owned(),
            ));
        }
        found = Some(parse_basic_string(value.trim())?);
    }
    let version = found.ok_or_else(|| {
        Error::InvalidCargoManifest(
            "root [package] must contain a literal version string".to_owned(),
        )
    })?;
    validate_version(&version)?;
    Ok(version)
}

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;
            continue;
        }
        if quoted && character == '\\' {
            escaped = true;
        } else if character == '"' {
            quoted = !quoted;
        } else if character == '#' && !quoted {
            return &line[..index];
        }
    }
    line
}

fn parse_basic_string(value: &str) -> Result<String> {
    if value.len() < 2 || !value.starts_with('"') || !value.ends_with('"') {
        return Err(Error::InvalidCargoManifest(
            "[package].version must be a direct basic string".to_owned(),
        ));
    }
    let inner = &value[1..value.len() - 1];
    if inner.contains('"') || inner.contains('\\') {
        return Err(Error::InvalidCargoManifest(
            "[package].version must not use escapes".to_owned(),
        ));
    }
    Ok(inner.to_owned())
}

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::InvalidVersion(version.to_owned()));
    }
    Ok(())
}

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

    #[test]
    fn parses_one_canonical_root_package_version() {
        let manifest = "[workspace]\nresolver = \"3\"\n\n[package]\nname = \"demo\"\nversion = \"12.3.4\" # current\n";
        assert_eq!(parse_manifest_version(manifest).unwrap(), "12.3.4");
    }

    #[test]
    fn rejects_noncanonical_and_inherited_versions() {
        for manifest in [
            "[package]\nversion.workspace = true\n",
            "[package]\nversion = \"1.2\"\n",
            "[package]\nversion = \"01.2.3\"\n",
            "[package]\nversion = \"1.2.3-beta\"\n",
        ] {
            assert!(parse_manifest_version(manifest).is_err());
        }
    }
}