portable-network-archive 0.36.1

Portable-Network-Archive cli
Documentation
use crate::{
    cli::{GlobalContext, PasswordArgs},
    command::{
        Command, ask_password,
        core::{collect_split_archives, run_read_entries},
    },
};
use clap::{Parser, ValueHint};
use pna::{Encryption, NormalEntry, ReadEntry, ReadOptions, SolidEntry};
use std::{io, path::PathBuf};

#[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[command(
    after_long_help = "Note: for encrypted entries, a wrong password is indistinguishable from corruption."
)]
pub(crate) struct VerifyCommand {
    #[arg(short = 'f', long = "file", help = "Archive file path", value_hint = ValueHint::FilePath)]
    archive: PathBuf,
    #[arg(
        long,
        help = "Verify chunk structure and CRC32 only, without decoding entry data",
        long_help = "Verify chunk structure and CRC32 only. No entry data is decoded, so neither the entries contained in a solid block nor corruption that leaves a chunk's CRC32 intact are checked. Every entry whose chunks are intact is counted as ok, and no password is required."
    )]
    fast: bool,
    #[command(flatten)]
    password: PasswordArgs,
}

impl Command for VerifyCommand {
    #[inline]
    fn execute(self, _: &GlobalContext) -> anyhow::Result<()> {
        verify_archive(self)
    }
}

#[derive(Default)]
struct VerifyReport {
    ok: usize,
    failed: usize,
    skipped: usize,
    encrypted_failure: bool,
}

impl VerifyReport {
    fn total(&self) -> usize {
        self.ok + self.failed + self.skipped
    }
}

fn verify_archive(args: VerifyCommand) -> anyhow::Result<()> {
    let fast = args.fast;
    let password = ask_password(args.password)?;
    let password = password.as_deref();
    let read_options = ReadOptions::with_password(password);
    let archives = collect_split_archives(&args.archive)?;
    let mut report = VerifyReport::default();
    let mut solid_blocks = 0usize;
    let mut resyncing = false;
    let result = run_read_entries(
        archives,
        |entry| {
            match entry {
                Err(err) if err.kind() == io::ErrorKind::InvalidData => {
                    // A broken chunk aborts the current entry's assembly, and
                    // the remainder of that entry surfaces as one or more
                    // InvalidData errors before the iterator resyncs on the
                    // next entry header. Count the corrupted entry once;
                    // adjacent corrupted entries with no healthy entry in
                    // between collapse into a single failure (accepted
                    // approximation).
                    if !resyncing {
                        println!("<corrupted entry>: FAILED ({err})");
                        report.failed += 1;
                        resyncing = true;
                    }
                    Ok(())
                }
                Err(err) => Err(err),
                Ok(read_entry) => {
                    resyncing = false;
                    if fast {
                        // Assembling the entry already validated every chunk's
                        // CRC32, which is all `--fast` checks: no entry data is
                        // decoded, so the entries inside a solid block are not
                        // verified either.
                        report.ok += 1;
                        return Ok(());
                    }
                    match read_entry {
                        ReadEntry::Solid(solid) => {
                            solid_blocks += 1;
                            if solid.encryption() != Encryption::NO && password.is_none() {
                                println!("<solid block #{solid_blocks}>: skipped (encrypted)");
                                report.skipped += 1;
                                return Ok(());
                            }
                            if let Err(err) =
                                verify_solid(&solid, password, &read_options, &mut report)
                            {
                                println!("<solid block #{solid_blocks}>: FAILED ({err})");
                                report.failed += 1;
                                report.encrypted_failure |= solid.encryption() != Encryption::NO;
                            }
                        }
                        ReadEntry::Normal(entry) => {
                            verify_entry(&entry, password, &read_options, &mut report)
                        }
                    }
                    Ok(())
                }
            }
        },
        false,
    );
    print_summary(&report);
    if let Err(err) = result {
        return Err(
            anyhow::Error::new(err).context("archive structure is broken; verification aborted")
        );
    }
    if report.failed > 0 {
        anyhow::bail!(
            "verification failed: {} of {} entries are corrupted",
            report.failed,
            report.total()
        );
    }
    Ok(())
}

fn verify_solid(
    solid: &SolidEntry,
    password: Option<&[u8]>,
    read_options: &ReadOptions,
    report: &mut VerifyReport,
) -> io::Result<()> {
    for entry in solid.entries(read_options)? {
        verify_entry(&entry?, password, read_options, report);
    }
    Ok(())
}

fn verify_entry(
    entry: &NormalEntry,
    password: Option<&[u8]>,
    read_options: &ReadOptions,
    report: &mut VerifyReport,
) {
    let encrypted = entry.header().encryption() != Encryption::NO;
    if encrypted && password.is_none() {
        // Decoding is impossible without the password.
        report.skipped += 1;
        return;
    }
    match read_through(entry, read_options) {
        Ok(size) => {
            if let Some(hint) = entry.metadata().raw_file_size()
                && hint != u128::from(size)
            {
                log::warn!(
                    "{}: size hint (fSIZ) mismatch: recorded {hint}, actual {size}",
                    entry.name()
                );
            }
            log::debug!("{}: ok", entry.name());
            report.ok += 1;
        }
        Err(err) => {
            println!("{}: FAILED ({err})", entry.name());
            report.failed += 1;
            report.encrypted_failure |= encrypted;
        }
    }
}

fn read_through(entry: &NormalEntry, read_options: &ReadOptions) -> io::Result<u64> {
    let mut reader = entry.reader(read_options)?;
    io::copy(&mut reader, &mut io::sink())
}

fn print_summary(report: &VerifyReport) {
    if report.skipped > 0 {
        println!(
            "{} entries skipped (encrypted; no password provided)",
            report.skipped
        );
    }
    println!(
        "total: {}, ok: {}, failed: {}, skipped (encrypted): {}",
        report.total(),
        report.ok,
        report.failed,
        report.skipped
    );
    if report.encrypted_failure {
        println!(
            "note: a wrong password is indistinguishable from corruption for encrypted entries"
        );
    }
}