brandon 0.1.0

Standalone Rust toolkit for Ethereum ERA/ERA1 files — read, write, verify, convert
Documentation
use std::path::Path;

use anyhow::{Context, bail};
use brandon::verify::hash::verify_file_against_manifest;
use serde::Serialize;

use crate::cli::{directory::Input, output};

#[derive(Serialize)]
pub struct VerifyResult {
    pub valid: bool,
    pub block_count: usize,
    pub state_present: bool,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
    pub manifest_match: Option<bool>,
}

impl std::fmt::Display for VerifyResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.valid {
            writeln!(f, "✓ valid")?;
        } else {
            writeln!(f, "✗ invalid")?;
        }
        writeln!(f, "  blocks:   {}", self.block_count)?;
        writeln!(
            f,
            "  state:    {}",
            if self.state_present {
                "present"
            } else {
                "absent"
            }
        )?;

        if let Some(matched) = self.manifest_match {
            if matched {
                writeln!(f, "  manifest: matches")?;
            } else {
                writeln!(f, "  manifest: MISMATCH")?;
            }
        }

        if !self.errors.is_empty() {
            writeln!(f)?;
            writeln!(f, "Errors:")?;
            for e in &self.errors {
                writeln!(f, "  - {e}")?;
            }
        }

        if !self.warnings.is_empty() {
            writeln!(f)?;
            writeln!(f, "Warnings:")?;
            for w in &self.warnings {
                writeln!(f, "  - {w}")?;
            }
        }

        Ok(())
    }
}

#[derive(Serialize)]
pub struct DirVerifyResult {
    pub path: String,
    pub checked: usize,
    pub passed: usize,
    pub failed: usize,
    pub failed_files: Vec<String>,
    pub total_size: u64,
    pub total_size_human: String,
}

impl std::fmt::Display for DirVerifyResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(
            f,
            "Checked {} files ({} total)",
            self.checked, self.total_size_human
        )?;
        if self.failed == 0 {
            writeln!(f, "✓ All {} files passed verification", self.passed)?;
        } else {
            writeln!(f, "{} files FAILED", self.failed)?;
            for file in &self.failed_files {
                writeln!(f, "  - {file}")?;
            }
        }
        Ok(())
    }
}

pub fn run(path: &str, manifest_path: Option<&str>, json: bool) -> anyhow::Result<()> {
    let input = Input::resolve(path)?;

    match input {
        Input::File(_) => run_single(path, manifest_path, json),
        Input::Dir(dir) => {
            if manifest_path.is_some() {
                bail!("--manifest cannot be used with a directory");
            }
            run_directory(&dir, json)
        }
    }
}

pub fn run_single(path: &str, manifest_path: Option<&str>, json: bool) -> anyhow::Result<()> {
    let data = std::fs::read(path).with_context(|| format!("cannot read {path}"))?;

    let vr = brandon::verify::verify_era(&data[..]);

    let mut manifest_match = None;

    if let Some(mp) = manifest_path {
        let file = Path::new(path);
        let manifest = Path::new(mp);
        let matched = verify_file_against_manifest(file, manifest)
            .with_context(|| format!("failed to verify against manifest {mp}"))?;
        manifest_match = Some(matched);
    }

    let valid = vr.valid && manifest_match.unwrap_or(true);

    let result = VerifyResult {
        valid,
        block_count: vr.block_count,
        state_present: vr.state_present,
        errors: vr.errors,
        warnings: vr.warnings,
        manifest_match,
    };

    output(&result, json);

    if !valid {
        bail!("verification failed");
    }

    Ok(())
}

fn run_directory(dir: &super::directory::ArchiveDirectory, json: bool) -> anyhow::Result<()> {
    let mut checked = 0usize;
    let mut passed = 0usize;
    let mut failed = 0usize;
    let mut failed_files = Vec::new();
    let mut total_size = 0u64;

    for (_, path) in &dir.files {
        let metadata = match std::fs::metadata(path) {
            Ok(m) => m,
            Err(_) => {
                failed += 1;
                failed_files.push(path.file_name().unwrap().to_str().unwrap().to_string());
                continue;
            }
        };
        total_size += metadata.len();
        checked += 1;

        match std::fs::read(path) {
            Ok(data) => {
                let vr = brandon::verify::verify_era(&data[..]);
                if vr.valid {
                    passed += 1;
                } else {
                    failed += 1;
                    failed_files.push(path.file_name().unwrap().to_str().unwrap().to_string());
                }
            }
            Err(_) => {
                failed += 1;
                failed_files.push(path.file_name().unwrap().to_str().unwrap().to_string());
            }
        }
    }

    let result = DirVerifyResult {
        path: dir.path.to_str().unwrap().to_string(),
        checked,
        passed,
        failed,
        failed_files,
        total_size,
        total_size_human: super::human_size(total_size),
    };

    output(&result, json);

    if failed > 0 {
        bail!("directory verification failed");
    }

    Ok(())
}