kcode-rust-libs 0.2.0

Manage, validate, document, and publish small agent-authored Rust crates
Documentation
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

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

pub(crate) fn validate_rust_lib_name(name: &str) -> Result<()> {
    let mut characters = name.chars();
    let valid_first = characters
        .next()
        .is_some_and(|character| character.is_ascii_alphanumeric());
    let valid_rest = characters
        .all(|character| character.is_ascii_alphanumeric() || character == '-' || character == '_');

    if !valid_first || !valid_rest {
        return Err(Error::InvalidRustLibName(name.to_owned()));
    }
    Ok(())
}

pub(crate) fn load_files(root: &Path) -> Result<Vec<RustLibFile>> {
    let mut files = Vec::new();
    load_directory(root, root, &mut files)?;
    files.sort_by(|left, right| left.path.cmp(&right.path));
    Ok(files)
}

fn load_directory(root: &Path, directory: &Path, files: &mut Vec<RustLibFile>) -> Result<()> {
    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 path = entry.path();
        let file_type = entry
            .file_type()
            .map_err(|source| Error::io("inspect directory entry", &path, source))?;

        if file_type.is_symlink() {
            return Err(Error::SymlinkNotAllowed(path));
        }
        if file_type.is_dir() {
            load_directory(root, &path, files)?;
            continue;
        }
        if !file_type.is_file() {
            return Err(Error::UnsupportedFileType(path));
        }

        let relative = to_rust_lib_path(root, &path)?;
        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(relative, contents));
    }

    Ok(())
}

fn to_rust_lib_path(root: &Path, path: &Path) -> Result<RustLibPath> {
    let relative = path
        .strip_prefix(root)
        .expect("a recursively loaded path must remain beneath its root");
    let mut text = String::new();

    for component in relative.components() {
        let component = component
            .as_os_str()
            .to_str()
            .ok_or_else(|| Error::NonUtf8Path(path.to_path_buf()))?;
        if !text.is_empty() {
            text.push('/');
        }
        text.push_str(component);
    }

    RustLibPath::new(text)
}

pub(crate) fn validate_write_target(root: &Path, path: &RustLibPath) -> Result<()> {
    let components: Vec<_> = path.as_str().split('/').collect();
    let mut current = root.to_path_buf();

    for (index, component) in components.iter().enumerate() {
        current.push(component);
        match fs::symlink_metadata(&current) {
            Ok(metadata) => {
                if metadata.file_type().is_symlink() {
                    return Err(Error::SymlinkNotAllowed(current));
                }
                let is_destination = index + 1 == components.len();
                if (!is_destination && !metadata.is_dir())
                    || (is_destination && !metadata.is_file())
                {
                    return Err(Error::UnsupportedFileType(current));
                }
            }
            Err(source) if source.kind() == io::ErrorKind::NotFound => break,
            Err(source) => return Err(Error::io("inspect write path", current, source)),
        }
    }

    Ok(())
}

pub(crate) fn write_file(root: &Path, file: &RustLibFile) -> Result<()> {
    let components: Vec<_> = file.path.as_str().split('/').collect();
    let mut current = root.to_path_buf();

    for component in &components[..components.len() - 1] {
        current.push(component);
        match fs::symlink_metadata(&current) {
            Ok(metadata) => {
                if metadata.file_type().is_symlink() {
                    return Err(Error::SymlinkNotAllowed(current));
                }
                if !metadata.is_dir() {
                    return Err(Error::UnsupportedFileType(current));
                }
            }
            Err(source) if source.kind() == io::ErrorKind::NotFound => fs::create_dir(&current)
                .map_err(|source| Error::io("create directory", &current, source))?,
            Err(source) => return Err(Error::io("inspect directory", current, source)),
        }
    }

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

    fs::write(&destination, file.contents.as_bytes())
        .map_err(|source| Error::io("write file", destination, source))
}

pub(crate) fn copy_tree(source: &Path, destination: &Path) -> Result<()> {
    fs::create_dir_all(destination)
        .map_err(|error| Error::io("create copied Rust library", 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 Rust library", source, error))?;

    for entry in entries {
        let entry = entry.map_err(|error| Error::io("read Rust library entry", source, error))?;
        let source_path = entry.path();
        let destination_path = destination.join(entry.file_name());
        let file_type = entry
            .file_type()
            .map_err(|error| Error::io("inspect Rust library entry", &source_path, error))?;

        if file_type.is_symlink() {
            return Err(Error::SymlinkNotAllowed(source_path));
        }
        if file_type.is_dir() {
            fs::create_dir(&destination_path).map_err(|error| {
                Error::io(
                    "create copied Rust library directory",
                    &destination_path,
                    error,
                )
            })?;
            copy_directory(&source_path, &destination_path)?;
        } else if file_type.is_file() {
            fs::copy(&source_path, &destination_path)
                .map_err(|error| Error::io("copy Rust library file", &source_path, error))?;
        } else {
            return Err(Error::UnsupportedFileType(source_path));
        }
    }

    Ok(())
}

pub(crate) fn absolute_lexical(path: &Path) -> Result<PathBuf> {
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .map_err(|source| Error::io("read current directory", ".", source))?
            .join(path)
    };

    let mut normalized = PathBuf::new();
    for component in absolute.components() {
        use std::path::Component;
        match component {
            Component::CurDir => {}
            Component::ParentDir => {
                normalized.pop();
            }
            _ => normalized.push(component.as_os_str()),
        }
    }
    Ok(normalized)
}

pub(crate) fn paths_overlap(left: &Path, right: &Path) -> bool {
    left == right || left.starts_with(right) || right.starts_with(left)
}