par2-rs 0.10.6

PAR2 parity verification and repair
Documentation
#[path = "../tests/support/benchmark_support.rs"]
mod benchmark_support;

use std::path::PathBuf;
use std::process::Command;
use std::time::Instant;

use benchmark_support::{
    DEFAULT_TURBO_BINARY, TurboMode, differential_scenarios, format_shell_command,
    select_scenarios, stage_scenario, turbo_args,
};
use par2_rs::{Par2RepairOutcome, Par2Repairer, Par2RepairerOptions};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Engine {
    Weaver,
    Turbo,
    Both,
}

impl Engine {
    fn includes_weaver(self) -> bool {
        matches!(self, Self::Weaver | Self::Both)
    }

    fn includes_turbo(self) -> bool {
        matches!(self, Self::Turbo | Self::Both)
    }
}

#[derive(Debug)]
struct Config {
    execute: bool,
    list_only: bool,
    engine: Engine,
    mode: TurboMode,
    scenario_filters: Vec<String>,
    turbo_binary: PathBuf,
    turbo_threads: Option<usize>,
    turbo_memory_mib: Option<usize>,
    scan_skip_data: bool,
    scan_skip_leeway: Option<u64>,
    quiet: bool,
}

fn usage() -> &'static str {
    "\
Usage:
  cargo run -p par2-rs --example diff_par2cmdline_turbo -- [options]

Options:
  --list                     List available scenarios and exit
  --execute                  Run the selected engines instead of dry-run planning
  --engine <weaver|turbo|both>
  --mode <verify|repair>
  --scenario <name-fragment> Repeatable scenario filter
  --turbo-binary <path>      Defaults to par2 on PATH
  --turbo-threads <n>        Passed to par2cmdline-turbo as -t<n>
  --turbo-memory-mib <n>     Passed to par2cmdline-turbo as -m<n>
  --scan-skip-data           Use oracle-style block skip scanning (-N in par2cmdline-turbo)
  --scan-skip-leeway <bytes> Use this skip leeway with --scan-skip-data (-S<n>)
  --no-quiet                 Omit -q when invoking par2cmdline-turbo
  --help
"
}

fn main() {
    let config = parse_config();
    let scenarios = select_scenarios(differential_scenarios(), &config.scenario_filters);

    if config.list_only {
        for scenario in &scenarios {
            println!("{}  {}", scenario.name, scenario.fixture_dir.display());
        }
        return;
    }

    println!("par2-rs differential scaffold");
    println!("  execute: {}", config.execute);
    println!("  engine:  {:?}", config.engine);
    println!("  mode:    {:?}", config.mode);
    println!("  turbo:   {}", config.turbo_binary.display());
    println!();

    for scenario in scenarios {
        let preview = stage_scenario(&scenario);
        println!("scenario: {}", preview.name);
        println!("  fixture: {}", scenario.fixture_dir.display());
        println!("  payloads: {}", preview.payload_paths.len());
        println!("  slice: {} KiB", preview.slice_size / 1024);

        if config.engine.includes_weaver() {
            if config.execute {
                let staged = stage_scenario(&scenario);
                let started = Instant::now();
                let outcome = run_weaver(
                    &staged,
                    config.mode,
                    config.scan_skip_data,
                    config.scan_skip_leeway,
                )
                .expect("weaver run");
                println!(
                    "  weaver: {:?} in {:.3}s (missing={} available={} reconstructed={} copied={})",
                    outcome.status,
                    started.elapsed().as_secs_f64(),
                    outcome.missing_blocks,
                    outcome.available_blocks,
                    outcome.bytes_reconstructed,
                    outcome.bytes_copied
                );
            } else {
                println!(
                    "  weaver: dry-run via Par2Repairer(repair={})",
                    matches!(config.mode, TurboMode::Repair)
                );
            }
        }

        if config.engine.includes_turbo() {
            if config.execute {
                let staged = stage_scenario(&scenario);
                let args = turbo_args(
                    &staged,
                    config.mode,
                    config.turbo_memory_mib,
                    config.turbo_threads,
                    config.quiet,
                    config.scan_skip_data,
                    config.scan_skip_leeway,
                );
                let started = Instant::now();
                let output = Command::new(&config.turbo_binary)
                    .args(&args)
                    .current_dir(staged.temp.path())
                    .output()
                    .expect("run par2cmdline-turbo");
                println!(
                    "  turbo: exit={} in {:.3}s",
                    output.status,
                    started.elapsed().as_secs_f64()
                );
                let stdout = String::from_utf8_lossy(&output.stdout);
                if !stdout.trim().is_empty() {
                    println!("  turbo stdout:\n{}", indent(stdout.trim_end()));
                }
                let stderr = String::from_utf8_lossy(&output.stderr);
                if !stderr.trim().is_empty() {
                    println!("  turbo stderr:\n{}", indent(stderr.trim_end()));
                }
            } else {
                let args = turbo_args(
                    &preview,
                    config.mode,
                    config.turbo_memory_mib,
                    config.turbo_threads,
                    config.quiet,
                    config.scan_skip_data,
                    config.scan_skip_leeway,
                );
                let preview =
                    format_shell_command(&config.turbo_binary, &args, preview.temp.path());
                println!("  turbo: {preview}");
            }
        }

        println!();
    }
}

fn parse_config() -> Config {
    let mut config = Config {
        execute: false,
        list_only: false,
        engine: Engine::Both,
        mode: TurboMode::Repair,
        scenario_filters: Vec::new(),
        turbo_binary: std::env::var_os("PAR2_TURBO_BINARY")
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from(DEFAULT_TURBO_BINARY)),
        turbo_threads: std::env::var("PAR2_TURBO_THREADS")
            .ok()
            .and_then(|value| value.parse().ok()),
        turbo_memory_mib: std::env::var("PAR2_TURBO_MEMORY_MIB")
            .ok()
            .and_then(|value| value.parse().ok()),
        scan_skip_data: false,
        scan_skip_leeway: None,
        quiet: true,
    };

    let mut args = std::env::args().skip(1);
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--list" => config.list_only = true,
            "--execute" => config.execute = true,
            "--engine" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --engine");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.engine = parse_engine(&value);
            }
            "--mode" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --mode");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.mode = parse_mode(&value);
            }
            "--scenario" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --scenario");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.scenario_filters.push(value);
            }
            "--turbo-binary" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --turbo-binary");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.turbo_binary = PathBuf::from(value);
            }
            "--turbo-threads" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --turbo-threads");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.turbo_threads = Some(value.parse().expect("valid thread count"));
            }
            "--turbo-memory-mib" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --turbo-memory-mib");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.turbo_memory_mib = Some(value.parse().expect("valid memory size"));
            }
            "--scan-skip-data" => config.scan_skip_data = true,
            "--scan-skip-leeway" => {
                let value = args.next().unwrap_or_else(|| {
                    eprintln!("missing value for --scan-skip-leeway");
                    eprintln!("{}", usage());
                    std::process::exit(2);
                });
                config.scan_skip_data = true;
                config.scan_skip_leeway = Some(value.parse().expect("valid skip leeway"));
            }
            "--no-quiet" => config.quiet = false,
            "--help" | "-h" => {
                println!("{}", usage());
                std::process::exit(0);
            }
            other => {
                eprintln!("unknown option: {other}");
                eprintln!("{}", usage());
                std::process::exit(2);
            }
        }
    }

    config
}

fn parse_engine(value: &str) -> Engine {
    match value {
        "weaver" => Engine::Weaver,
        "turbo" => Engine::Turbo,
        "both" => Engine::Both,
        _ => {
            eprintln!("invalid --engine value: {value}");
            eprintln!("{}", usage());
            std::process::exit(2);
        }
    }
}

fn parse_mode(value: &str) -> TurboMode {
    match value {
        "verify" => TurboMode::Verify,
        "repair" => TurboMode::Repair,
        _ => {
            eprintln!("invalid --mode value: {value}");
            eprintln!("{}", usage());
            std::process::exit(2);
        }
    }
}

fn run_weaver(
    staged: &benchmark_support::StagedScenario,
    mode: TurboMode,
    scan_skip_data: bool,
    scan_skip_leeway: Option<u64>,
) -> par2_rs::Result<Par2RepairOutcome> {
    let mut options = Par2RepairerOptions::new(
        staged.temp.path().to_path_buf(),
        vec![staged.main_par2.clone()],
    );
    options.recovery_paths = staged.recovery_par2.clone();
    options.repair = matches!(mode, TurboMode::Repair);
    options.scan_skip_data = scan_skip_data;
    if let Some(scan_skip_leeway) = scan_skip_leeway {
        options.scan_skip_leeway = scan_skip_leeway;
    }
    Par2Repairer::new(options).verify_or_repair()
}

fn indent(text: &str) -> String {
    text.lines()
        .map(|line| format!("    {line}"))
        .collect::<Vec<_>>()
        .join("\n")
}