dirpack 0.2.0

Budgeted directory indexes for AI coding agents with tree-sitter signatures
Documentation
use std::path::{Component, Path, PathBuf};

use thiserror::Error;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveEntryType {
    File,
    Directory,
    Symlink,
    Hardlink,
    Other,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum ArchiveValidationError {
    #[error("entry path is empty")]
    EmptyPath,
    #[error("entry path is absolute")]
    AbsolutePath,
    #[error("entry path contains parent traversal")]
    Traversal,
    #[error("entry path too long")]
    PathTooLong,
    #[error("entry type not allowed")]
    UnsupportedType,
    #[error("entry path escapes sandbox")]
    EscapesSandbox,
}

pub fn validate_archive_entry(
    root: &Path,
    entry_path: &Path,
    entry_type: ArchiveEntryType,
    max_path_len: usize,
) -> Result<PathBuf, ArchiveValidationError> {
    if entry_path.as_os_str().is_empty() {
        return Err(ArchiveValidationError::EmptyPath);
    }

    let mut normalized = PathBuf::new();
    for component in entry_path.components() {
        match component {
            Component::ParentDir => return Err(ArchiveValidationError::Traversal),
            Component::RootDir | Component::Prefix(_) => {
                return Err(ArchiveValidationError::AbsolutePath)
            }
            Component::CurDir => {}
            Component::Normal(part) => normalized.push(part),
        }
    }

    if normalized.as_os_str().is_empty() {
        return Err(ArchiveValidationError::EmptyPath);
    }

    if normalized.as_os_str().len() > max_path_len {
        return Err(ArchiveValidationError::PathTooLong);
    }

    match entry_type {
        ArchiveEntryType::File | ArchiveEntryType::Directory => {}
        ArchiveEntryType::Symlink | ArchiveEntryType::Hardlink | ArchiveEntryType::Other => {
            return Err(ArchiveValidationError::UnsupportedType)
        }
    }

    let target = root.join(&normalized);
    if target.strip_prefix(root).is_err() {
        return Err(ArchiveValidationError::EscapesSandbox);
    }

    Ok(target)
}