sigmate 1.0.0

A modern, developer-focused CLI for cryptographic file signing and verification.
Documentation
use crate::app_config::{ConfigManager, SIGNATURES_FOLDER};
use crate::cli::VerifyArgs;
use crate::error::AppError;
use crate::services::{VerificationResult, VerificationService};
use crate::ui::logger::{print_json, Logger};
use crate::utils::files::{self, sanitize_filename};
use serde_json::json;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

pub fn execute(args: VerifyArgs, logger: &Logger) -> Result<ExitCode, AppError> {
    let is_checksum_mode = args.checksum_file.is_some();
    let effective_key_path = resolve_key_path(&args)?;

    if effective_key_path.is_none() && !is_checksum_mode {
        return Err(AppError::InvalidInput(
            "Must specify either --key/--signer or --checksum-file.".into(),
        ));
    }

    let base_dir = args
        .walk
        .clone()
        .or_else(|| {
            args.target_path_or_file.as_ref().map(|p| {
                if p.is_dir() {
                    p.clone()
                } else {
                    p.parent().unwrap_or(Path::new("")).to_path_buf()
                }
            })
        })
        .unwrap_or_else(|| {
            if let Some(checksum_path) = &args.checksum_file {
                checksum_path.parent().unwrap_or(Path::new("")).to_path_buf()
            } else {
                std::env::current_dir().expect("Cannot get current directory")
            }
        });

    let files_to_verify = find_files_to_verify(&args, logger)?;

    if let Some(key_path) = effective_key_path {
        if files_to_verify.is_empty() {
            if !args.json {
                logger.warn("No files found to verify.", Some("🤷"));
            }
            return Ok(ExitCode::SUCCESS);
        }
        let key_data =
            fs::read(&key_path).map_err(|e| AppError::Io { path: key_path, source: e })?;
        let sig_dir = args
            .signatures_input
            .clone()
            .unwrap_or_else(|| base_dir.join(SIGNATURES_FOLDER));

        let service = VerificationService {
            files: &files_to_verify,
            key_data: &key_data,
            sig_type: &args.sig_type,
            sig_dir: &sig_dir,
            base_dir: &base_dir,
            require_trusted: args.require_trusted,
        };
        let results = service.verify_signatures()?;
        print_crypto_results(&results, args.json, logger);
        if results.iter().all(|r| r.overall_verified) {
            Ok(ExitCode::SUCCESS)
        } else {
            Ok(ExitCode::FAILURE)
        }
    } else if let Some(checksum_path) = &args.checksum_file {
        let service = VerificationService {
            files: &files_to_verify,
            key_data: &[],
            sig_type: "",
            sig_dir: checksum_path.parent().unwrap(),
            base_dir: &base_dir,
            require_trusted: false,
        };
        let results = service.verify_checksums(checksum_path, &args.checksum_algo)?;
        print_checksum_results(&results, args.json, logger);
        if results.iter().all(|r| r.overall_verified) {
            Ok(ExitCode::SUCCESS)
        } else {
            Ok(ExitCode::FAILURE)
        }
    } else {
        unreachable!();
    }
}

fn resolve_key_path(args: &VerifyArgs) -> Result<Option<PathBuf>, AppError> {
    if let Some(key_path) = &args.key {
        return Ok(Some(key_path.clone()));
    }
    if let Some(signer_name) = &args.signer {
        let config = ConfigManager::new()?;
        let keyring_path = config.get_keyring_path()?;
        let key_alias = sanitize_filename(signer_name);
        let potential_key_path = keyring_path.join(format!("{}.pub", key_alias));
        if !potential_key_path.is_file() {
            return Err(AppError::InvalidInput(format!(
                "Signer '{}' not found in keyring: {}",
                signer_name,
                potential_key_path.display()
            )));
        }
        return Ok(Some(potential_key_path));
    }
    Ok(None)
}

fn find_files_to_verify(
    args: &VerifyArgs,
    logger: &Logger,
) -> Result<Vec<PathBuf>, AppError> {
    if let Some(walk_path) = &args.walk {
        return files::collect_files(walk_path, logger);
    }
    if let Some(list_path) = &args.file_list {
        let paths_from_list = files::read_file_list(list_path)?;
        let mut all_files = Vec::new();
        let list_base = list_path.parent().unwrap_or(Path::new(""));
        for path in paths_from_list {
            let full_path = list_base.join(path);
            if full_path.is_dir() {
                all_files.extend(files::collect_files(&full_path, logger)?);
            } else if full_path.is_file() {
                all_files.push(full_path);
            }
        }
        return Ok(all_files);
    }
    if let Some(target) = &args.target_path_or_file {
        if target.is_dir() {
            return files::collect_files(target, logger);
        } else {
            return Ok(vec![target.clone()]);
        }
    }
    if args.checksum_file.is_some() {
        return Ok(vec![]);
    }
    Ok(vec![])
}

fn print_crypto_results(results: &[VerificationResult], json_output: bool, logger: &Logger) {
    if json_output {
        print_json(&json!(results));
        return;
    }

    for (i, result) in results.iter().enumerate() {
        let file_path = result.file.file_name().unwrap_or_default().to_string_lossy();
        if result.overall_verified {
            logger.success(&format!("VERIFIED: {}", file_path), Some(""));
        } else {
            logger.error(&format!("FAILED: {}", file_path), Some(""));
        }

        let source = result
            .metadata_source
            .as_ref()
            .or(result.signature_file.as_ref());
        if let Some(s) = source {
            logger.info(
                &format!(
                    "  ├─ Source: {}",
                    s.file_name().unwrap_or_default().to_string_lossy()
                ),
                None,
            );
        }
        if result.expected_hash.is_some() {
            if result.actual_hash == result.expected_hash {
                logger.success("  ├─ Content Hash:  Matches", None);
            } else {
                logger.error("  ├─ Content Hash:  MISMATCH", None);
                logger.error(
                    &format!(
                        "  │  ├─ Expected: {}",
                        result.expected_hash.as_deref().unwrap_or("")
                    ),
                    None,
                );
                logger.error(
                    &format!(
                        "  │  └─ Actual:   {}",
                        result.actual_hash.as_deref().unwrap_or("")
                    ),
                    None,
                );
            }
        }
        if result.valid_signature {
            logger.success("  ├─ Signature:       Valid", None);
        } else if source.is_some() {
            logger.error("  ├─ Signature:       INVALID", None);
        }

        if let Some(expired) = result.expired {
            if expired {
                logger.warn("  ├─ Expiration:    EXPIRED", None);
            } else {
                logger.success("  ├─ Expiration:    OK", None);
            }
        }
        if let Some(trusted) = result.trusted_signer {
            if trusted {
                logger.success("  └─ Signer Trust:  Trusted", None);
            } else {
                logger.warn("  └─ Signer Trust:  NOT TRUSTED", None);
            }
        }
        if let Some(err) = &result.error {
            logger.error(&format!("  └─ Error: {}", err), None);
        }

        if i < results.len() - 1 {
            println!();
        }
    }
}

fn print_checksum_results(results: &[VerificationResult], json_output: bool, logger: &Logger) {
    if json_output {
        print_json(&json!(results));
        return;
    }
    for result in results {
        let file_path = result.file.file_name().unwrap_or_default().to_string_lossy();
        if result.overall_verified {
            logger.success(
                &format!("MATCH: {} ({})", file_path, result.algorithm_used.as_deref().unwrap_or("")),
                Some(""),
            );
        } else {
            logger.error(&format!("MISMATCH: {}", file_path), Some(""));
            if let (Some(expected), Some(actual)) = (&result.expected_hash, &result.actual_hash) {
                logger.error(&format!("  ├─ Expected: {}", expected), None);
                logger.error(&format!("  └─ Actual:   {}", actual), None);
            }
            if let Some(err) = &result.error {
                logger.error(&format!("  └─ Error: {}", err), None);
            }
        }
    }
}