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, without decoding entry data. Solid blocks are still decoded because enumerating their entries requires decompression and decryption, so stream corruption inside a solid block is detected even with --fast. Encrypted normal entries are counted as ok because nothing is decoded, so the skipped category does not apply."
)]
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 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 => {
if !resyncing {
println!("<corrupted entry>: FAILED ({err})");
report.failed += 1;
resyncing = true;
}
Ok(())
}
Err(err) => Err(err),
Ok(read_entry) => {
resyncing = false;
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, fast, &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, fast, &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]>,
fast: bool,
report: &mut VerifyReport,
) -> io::Result<()> {
for entry in solid.entries(password)? {
verify_entry(&entry?, password, fast, report);
}
Ok(())
}
fn verify_entry(
entry: &NormalEntry,
password: Option<&[u8]>,
fast: bool,
report: &mut VerifyReport,
) {
if fast {
report.ok += 1;
return;
}
let encrypted = entry.header().encryption() != Encryption::No;
if encrypted && password.is_none() {
report.skipped += 1;
return;
}
match read_through(entry, password) {
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, password: Option<&[u8]>) -> io::Result<u64> {
let mut reader = entry.reader(ReadOptions::with_password(password))?;
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"
);
}
}