sigmate 1.0.0

A modern, developer-focused CLI for cryptographic file signing and verification.
Documentation
use crate::error::AppError;
use crate::ui::logger::Logger;
use ignore::WalkBuilder;
use std::fs;
use std::path::{Path, PathBuf};

const DEFAULT_EXCLUDED_EXTENSIONS: &[&str] = &["sig", "sigmeta.json"];

pub fn should_skip_file(path: &Path) -> bool {
    let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
    let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
    file_name.starts_with('.') || DEFAULT_EXCLUDED_EXTENSIONS.contains(&extension)
}

pub fn collect_files(base_dir: &Path, logger: &Logger) -> Result<Vec<PathBuf>, AppError> {
    logger.debug(
        &format!("Collecting files from {}", base_dir.display()),
        Some("📁"),
    );

    let mut collected = Vec::new();
    let mut walker = WalkBuilder::new(base_dir);
    walker
        .standard_filters(true)
        .hidden(true)
        .add_custom_ignore_filename(".sigmateignore");

    for result in walker.build() {
        match result {
            Ok(entry) => {
                let path = entry.path();
                if path.is_file() && !should_skip_file(path) {
                    collected.push(path.to_path_buf());
                }
            }
            Err(e) => {
                logger.warn(&format!("Error walking directory: {}", e), Some("⚠️"));
            }
        }
    }
    Ok(collected)
}

pub fn build_output_path(
    file_path: &Path,
    content_base_dir: &Path,
    extension_suffix: &str,
    output_root_dir: &Path,
) -> Result<PathBuf, AppError> {
    let relative_path = file_path
        .strip_prefix(content_base_dir)
        .unwrap_or(Path::new(file_path.file_name().unwrap_or_default()));

    let mut artifact_output_path = output_root_dir.join(relative_path);
    let original_extension = artifact_output_path
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or("");
    let new_extension = format!("{}{}", original_extension, extension_suffix);
    artifact_output_path.set_extension(new_extension);

    if let Some(parent) = artifact_output_path.parent() {
        fs::create_dir_all(parent).map_err(|e| AppError::Io {
            path: parent.to_path_buf(),
            source: e,
        })?;
    }

    Ok(artifact_output_path)
}

pub fn sanitize_filename(name: &str) -> String {
    name.chars()
        .map(|c| match c {
            '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' | ' ' | '.' => '_',
            _ => c,
        })
        .collect()
}

pub fn read_file_list(path: &Path) -> Result<Vec<PathBuf>, AppError> {
    let content = fs::read_to_string(path).map_err(|e| AppError::Io {
        path: path.to_path_buf(),
        source: e,
    })?;
    Ok(content
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
        .map(PathBuf::from)
        .collect())
}

pub fn remove_dot_slash_prefix(p: &Path) -> PathBuf {
    if let Ok(stripped) = p.strip_prefix("./") {
        stripped.to_path_buf()
    } else {
        p.to_path_buf()
    }
}