void-cli 0.0.4

CLI for void — anonymous encrypted source control
//! Repository integrity check (fsck).
//!
//! Verifies the integrity of the repository by checking:
//! - HEAD validity
//! - Commit chain integrity
//! - Metadata bundle validation
//! - Shard existence and decryption
//! - Index validation

use std::path::Path;

use serde::Serialize;
use void_core::ops::fsck::{self as core_fsck, FsckError, FsckOptions, FsckWarning};

use crate::context::{open_repo, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};

/// Command-line arguments for fsck.
#[derive(Debug)]
pub struct FsckArgs {
    /// Run full verification (verify content hashes).
    pub full: bool,
    /// Find unreferenced objects (gc candidates).
    pub unreferenced: bool,
}

/// JSON output for the fsck command.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FsckOutput {
    /// Whether the repository passed all checks.
    pub ok: bool,
    /// Critical errors found (as strings).
    pub errors: Vec<String>,
    /// Warnings found (as strings).
    pub warnings: Vec<String>,
    /// Statistics about the check.
    pub stats: FsckStatsOutput,
}

/// Statistics from the integrity check.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FsckStatsOutput {
    /// Total objects checked.
    pub objects_checked: usize,
    /// Objects that passed verification.
    pub objects_valid: usize,
    /// Objects that were missing.
    pub objects_missing: usize,
    /// Objects that were corrupt.
    pub objects_corrupt: usize,
}

fn format_error(err: &FsckError) -> String {
    match err {
        FsckError::MissingObject { cid, referenced_by } => {
            format!("MissingObject: CID {} referenced by {}", cid, referenced_by)
        }
        FsckError::CorruptObject { cid, reason } => {
            format!("CorruptObject: CID {}: {}", cid, reason)
        }
        FsckError::InvalidHead { reason } => {
            format!("InvalidHead: {}", reason)
        }
        FsckError::InvalidIndex { reason } => {
            format!("InvalidIndex: {}", reason)
        }
        FsckError::OrphanedLock => "OrphanedLock: Stale lock file exists".to_string(),
        FsckError::DataLoss {
            commit_cid,
            parent_cid,
            commit_files,
            parent_files,
        } => {
            format!(
                "DataLoss: Commit {} has {} files, parent {} had {}",
                commit_cid, commit_files, parent_cid, parent_files
            )
        }
    }
}

fn format_warning(warn: &FsckWarning) -> String {
    match warn {
        FsckWarning::UnreferencedObject { cid } => {
            format!(
                "UnreferencedObject: CID {} is unreferenced (gc candidate)",
                cid
            )
        }
        FsckWarning::StaleManifest => "StaleManifest: Manifest file is stale".to_string(),
        FsckWarning::UnexpectedFileDrop {
            commit_cid,
            parent_cid,
            commit_files,
            parent_files,
        } => {
            format!(
                "UnexpectedFileDrop: Commit {} has {} files, parent {} had {}",
                commit_cid, commit_files, parent_cid, parent_files
            )
        }
    }
}

/// Run the fsck command.
///
/// # Arguments
///
/// * `cwd` - Current working directory
/// * `args` - Fsck arguments
/// * `opts` - CLI options
pub fn run(cwd: &Path, args: FsckArgs, opts: &CliOptions) -> Result<(), CliError> {
    run_command("fsck", opts, |ctx| {
        ctx.progress("Running repository integrity check...");

        let repo = open_repo(cwd)?;

        let options = FsckOptions {
            find_unreferenced: args.unreferenced,
            verify_content_hashes: args.full,
            observer: None,
        };

        let result = core_fsck::fsck(repo.context(), &options).map_err(void_err_to_cli)?;

        // Human-readable output
        if !ctx.use_json() {
            if result.ok {
                ctx.info("Repository is healthy.");
            } else {
                ctx.info("Repository has issues:");
            }

            if !result.errors.is_empty() {
                ctx.info(format!("\nErrors ({}):", result.errors.len()));
                for err in &result.errors {
                    let formatted = format_error(err);
                    ctx.info(format!("  - {}", formatted));
                }
            }

            if !result.warnings.is_empty() {
                ctx.info(format!("\nWarnings ({}):", result.warnings.len()));
                for warn in &result.warnings {
                    let formatted = format_warning(warn);
                    ctx.info(format!("  - {}", formatted));
                }
            }

            ctx.info(format!(
                "\nStatistics: {} objects checked, {} valid, {} missing, {} corrupt",
                result.stats.objects_checked,
                result.stats.objects_valid,
                result.stats.objects_missing,
                result.stats.objects_corrupt
            ));
        }

        Ok(FsckOutput {
            ok: result.ok,
            errors: result.errors.iter().map(format_error).collect(),
            warnings: result.warnings.iter().map(format_warning).collect(),
            stats: FsckStatsOutput {
                objects_checked: result.stats.objects_checked,
                objects_valid: result.stats.objects_valid,
                objects_missing: result.stats.objects_missing,
                objects_corrupt: result.stats.objects_corrupt,
            },
        })
    })
}