par2-rs 0.10.6

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

use std::fs;
use std::path::{Path, PathBuf};

use par2_rs::{
    BlockSizing, Par2Creator, Par2CreatorOptions, Par2FileSet, Par2Repairer, Par2RepairerOptions,
    RecoveryAmount, VolumeScheme,
};
use tempfile::tempdir;

fn par2_files(directory: &Path, stem: &str) -> Vec<PathBuf> {
    let mut paths = fs::read_dir(directory)
        .unwrap()
        .map(|entry| entry.unwrap().path())
        .filter(|path| {
            path.extension()
                .is_some_and(|extension| extension.eq_ignore_ascii_case("par2"))
                && path
                    .file_name()
                    .is_some_and(|name| name.to_string_lossy().starts_with(stem))
        })
        .collect::<Vec<_>>();
    paths.sort();
    paths
}

#[test]
#[ignore = "requires a direct PAR2 compatibility executable"]
fn creation_interoperates_in_both_directions() {
    let binaries = support::par2_interop_binaries();
    assert!(
        !binaries.is_empty(),
        "no PAR2 interoperability binary is configured; set PAR2_INTEROP_BINARIES to a platform path list or PAR2_TURBO_BINARY to one executable"
    );

    let directory = tempdir().unwrap();
    fs::write(directory.path().join("empty.bin"), []).unwrap();
    fs::write(directory.path().join("odd.bin"), [1, 2, 3, 4, 5]).unwrap();
    fs::write(
        directory.path().join("payload.bin"),
        (0..4096)
            .map(|index| (index * 17) as u8)
            .collect::<Vec<_>>(),
    )
    .unwrap();

    let mut local_options = Par2CreatorOptions::with_output(
        directory.path().join("local-set"),
        Some(directory.path().to_path_buf()),
        vec![
            directory.path().join("empty.bin"),
            directory.path().join("odd.bin"),
            directory.path().join("payload.bin"),
        ],
    );
    local_options.block_sizing = BlockSizing::Bytes(4);
    local_options.recovery_amount = RecoveryAmount::Count(8);
    local_options.volume_count = Some(3);
    local_options.volume_scheme = VolumeScheme::Variable;
    let local_creator = Par2Creator::new(local_options);
    let local_plan = local_creator.plan().unwrap();
    assert_eq!(local_plan.sources.len(), 2);
    assert!(
        local_plan
            .sources
            .iter()
            .all(|source| source.par2_name != "empty.bin")
    );
    assert_eq!(
        local_plan
            .volumes
            .iter()
            .map(|volume| volume.recovery_count)
            .collect::<Vec<_>>(),
        vec![3, 3, 2]
    );
    let local_outcome = local_creator.create(&local_plan).unwrap();

    for (index, binary) in binaries.iter().enumerate() {
        let binary_label = binary.display();
        let verify_local = support::run_par2_binary(
            binary,
            directory.path(),
            &["verify", local_outcome.main_path.to_str().unwrap()],
        );
        assert!(
            verify_local.status.success(),
            "{binary_label}: external verification of local output failed: {}",
            String::from_utf8_lossy(&verify_local.stderr)
        );

        fs::remove_file(directory.path().join("odd.bin")).unwrap();
        let repair_local = support::run_par2_binary(
            binary,
            directory.path(),
            &["repair", local_outcome.main_path.to_str().unwrap()],
        );
        assert!(
            repair_local.status.success(),
            "{binary_label}: external repair of local output failed: {}",
            String::from_utf8_lossy(&repair_local.stderr)
        );
        assert_eq!(
            fs::read(directory.path().join("odd.bin")).unwrap(),
            [1, 2, 3, 4, 5]
        );

        let external_stem = format!("external-set-{index}");
        let external_output = format!("{external_stem}.par2");
        let external_create_args = vec![
            "create",
            "-r150",
            "-n3",
            external_output.as_str(),
            "empty.bin",
            "odd.bin",
            "payload.bin",
        ];
        let external_create =
            support::run_par2_binary(binary, directory.path(), &external_create_args);
        assert!(
            external_create.status.success(),
            "{binary_label}: external creation failed: {}",
            String::from_utf8_lossy(&external_create.stderr)
        );
        let external_paths = par2_files(directory.path(), &external_stem);
        assert!(external_paths.len() > 1);
        let external_set = Par2FileSet::from_paths(&external_paths).unwrap();
        let mut local_options =
            Par2RepairerOptions::new(directory.path().to_path_buf(), external_paths.clone());
        local_options.file_set = Some(external_set);
        let verified = Par2Repairer::new(local_options).verify_or_repair().unwrap();
        assert_eq!(verified.verification.total_missing_blocks, 0);

        fs::remove_file(directory.path().join("odd.bin")).unwrap();
        let mut repair_options =
            Par2RepairerOptions::new(directory.path().to_path_buf(), external_paths);
        repair_options.repair = true;
        let repaired = Par2Repairer::new(repair_options)
            .verify_or_repair()
            .unwrap();
        assert_eq!(
            fs::read(directory.path().join("odd.bin")).unwrap(),
            [1, 2, 3, 4, 5]
        );
        assert!(matches!(
            repaired.status,
            par2_rs::Par2RepairStatus::Repaired | par2_rs::Par2RepairStatus::Verified
        ));
    }
}