zipatch-rs 1.2.0

Parser for FFXIV ZiPatch patch files
Documentation
//! Integration tests for `src/index/source.rs` — the [`PatchSource`] trait
//! and its built-in [`MemoryPatchSource`] / [`FilePatchSource`] implementations.

use zipatch_rs::MemoryPatchSource;
use zipatch_rs::ZiPatchError;
use zipatch_rs::index::{FilePatchSource, PatchSource};

// ---- MemoryPatchSource ----

#[test]
fn memory_patch_source_new_wraps_single_buffer() {
    let bytes: Vec<u8> = (0..16u8).collect();
    let mut src = MemoryPatchSource::new(bytes.clone());
    assert_eq!(src.patch_count(), 1);

    let mut buf = [0u8; 8];
    src.read(0, 0, &mut buf).unwrap();
    assert_eq!(&buf, &bytes[..8]);
}

#[test]
fn memory_patch_source_from_slice_copies_data() {
    let bytes: Vec<u8> = (0..16u8).collect();
    let mut src = MemoryPatchSource::from_slice(&bytes);
    assert_eq!(src.patch_count(), 1);

    let mut buf = [0u8; 16];
    src.read(0, 0, &mut buf).unwrap();
    assert_eq!(&buf, bytes.as_slice());
}

#[test]
fn memory_patch_source_new_chain_patch_count() {
    let p0 = vec![0xAAu8; 8];
    let p1 = vec![0xBBu8; 8];
    let src = MemoryPatchSource::new_chain(vec![p0, p1]);
    assert_eq!(src.patch_count(), 2);
}

#[test]
fn memory_patch_source_offset_overflow_returns_too_short() {
    // usize::try_from(u64::MAX) fails on 32-bit, and on 64-bit the subsequent
    // end computation still overflows via checked_add. We use a large offset
    // that cannot represent a valid start position even on 64-bit platforms.
    let mut src = MemoryPatchSource::new(vec![0u8; 16]);
    let mut buf = [0u8; 4];
    let err = src
        .read(0, u64::MAX, &mut buf)
        .expect_err("u64::MAX offset must error");
    assert!(
        matches!(err, ZiPatchError::PatchSourceTooShort { .. }),
        "expected TooShort, got {err:?}"
    );
}

// ---- FilePatchSource ----

#[test]
fn file_patch_source_from_file_wraps_open_handle() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().join("patch.bin");
    std::fs::write(&path, b"ABCDEFGH").unwrap();

    let f = std::fs::File::open(&path).unwrap();
    let mut src = FilePatchSource::from_file(f);
    assert_eq!(src.patch_count(), 1);

    let mut buf = [0u8; 4];
    src.read(0, 0, &mut buf).unwrap();
    assert_eq!(&buf, b"ABCD");
}

#[test]
fn file_patch_source_patch_count_multi() {
    let tmp = tempfile::tempdir().unwrap();
    let p0 = tmp.path().join("p0.bin");
    let p1 = tmp.path().join("p1.bin");
    let p2 = tmp.path().join("p2.bin");
    std::fs::write(&p0, b"P0").unwrap();
    std::fs::write(&p1, b"P1").unwrap();
    std::fs::write(&p2, b"P2").unwrap();

    let src = FilePatchSource::open_chain([&p0, &p1, &p2]).unwrap();
    assert_eq!(src.patch_count(), 3);
}

/// C4: `FilePatchSource::open_chain` with an empty iterator. Pre-polish the
/// suite had no zero-chain coverage at all — a chain with `patch_count == 0`
/// has to reject reads with the documented `PatchIndexOutOfRange` rather than
/// panicking on the underlying `Vec::get`.
#[test]
fn file_patch_source_open_chain_empty_iterator_yields_empty_source() {
    let paths: Vec<std::path::PathBuf> = Vec::new();
    let mut src = FilePatchSource::open_chain(paths).expect("empty open_chain must succeed");
    assert_eq!(src.patch_count(), 0);

    let mut buf = [0u8; 0];
    let err = src
        .read(0, 0, &mut buf)
        .expect_err("read against empty chain must surface PatchIndexOutOfRange");
    match err {
        ZiPatchError::PatchIndexOutOfRange { patch, count } => {
            assert_eq!(patch, 0);
            assert_eq!(count, 0);
        }
        other => panic!("expected PatchIndexOutOfRange, got {other:?}"),
    }
}