zipatch-rs 1.7.0

Parser for FFXIV ZiPatch patch files
Documentation
//! End-to-end `ApplyConfig::apply_patch` coverage — moved from `src/lib.rs`
//! inline tests during the API-overhaul pass. Exercises the happy path and
//! parse/apply error propagation against the public crate-root surface.

use std::io::Cursor;
use zipatch_rs::test_utils::{MAGIC, make_chunk};
use zipatch_rs::{ApplyConfig, ApplyError, ParseError, ZiPatchReader};

#[test]
fn apply_to_applies_adir_chunk_to_filesystem() {
    // Verify that a well-formed ADIR + EOF_ patch creates the directory.
    let mut adir_body = Vec::new();
    adir_body.extend_from_slice(&7u32.to_be_bytes());
    adir_body.extend_from_slice(b"created");

    let mut patch = Vec::new();
    patch.extend_from_slice(&MAGIC);
    patch.extend_from_slice(&make_chunk(b"ADIR", &adir_body));
    patch.extend_from_slice(&make_chunk(b"EOF_", &[]));

    let tmp = tempfile::tempdir().unwrap();
    let ctx = ApplyConfig::new(tmp.path());
    let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
    ctx.apply_patch(reader).unwrap();

    assert!(
        tmp.path().join("created").is_dir(),
        "ADIR must have created the directory"
    );
}

#[test]
fn apply_to_empty_patch_succeeds_without_side_effects() {
    // MAGIC + EOF_ only: apply_to must return Ok(()) with no filesystem changes.
    let mut patch = Vec::new();
    patch.extend_from_slice(&MAGIC);
    patch.extend_from_slice(&make_chunk(b"EOF_", &[]));

    let tmp = tempfile::tempdir().unwrap();
    let ctx = ApplyConfig::new(tmp.path());
    let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
    ctx.apply_patch(reader).unwrap();
    // No new entries should appear in the temp dir.
    let entries: Vec<_> = std::fs::read_dir(tmp.path()).unwrap().collect();
    assert!(
        entries.is_empty(),
        "empty patch must not create any files/dirs"
    );
}

#[test]
fn apply_to_propagates_parse_error_as_unknown_chunk_tag() {
    // ZZZZ is not a known tag; apply_to must surface UnknownChunkTag.
    let mut patch = Vec::new();
    patch.extend_from_slice(&MAGIC);
    patch.extend_from_slice(&make_chunk(b"ZZZZ", &[]));

    let tmp = tempfile::tempdir().unwrap();
    let ctx = ApplyConfig::new(tmp.path());
    let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
    let err = ctx.apply_patch(reader).unwrap_err();
    assert!(
        matches!(err, ApplyError::Parse(ParseError::UnknownChunkTag(_))),
        "expected UnknownChunkTag, got {err:?}"
    );
}

#[test]
fn apply_to_propagates_apply_error_from_delete_directory() {
    // DELD on a non-existent directory without ignore_missing must return Io.
    let mut deld_body = Vec::new();
    deld_body.extend_from_slice(&14u32.to_be_bytes());
    deld_body.extend_from_slice(b"does_not_exist");

    let mut patch = Vec::new();
    patch.extend_from_slice(&MAGIC);
    patch.extend_from_slice(&make_chunk(b"DELD", &deld_body));
    patch.extend_from_slice(&make_chunk(b"EOF_", &[]));

    let tmp = tempfile::tempdir().unwrap();
    let ctx = ApplyConfig::new(tmp.path());
    let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
    let err = ctx.apply_patch(reader).unwrap_err();
    assert!(
        matches!(err, ApplyError::Io { .. }),
        "expected ApplyError::Io for missing dir without ignore_missing, got {err:?}"
    );
}

#[test]
fn default_no_observer_apply_succeeds_as_before() {
    // Regression: without with_observer the apply must succeed exactly as
    // it did before the observer API was introduced.
    let mut adir_body = Vec::new();
    adir_body.extend_from_slice(&7u32.to_be_bytes());
    adir_body.extend_from_slice(b"created");

    let mut patch = Vec::new();
    patch.extend_from_slice(&MAGIC);
    patch.extend_from_slice(&make_chunk(b"ADIR", &adir_body));
    patch.extend_from_slice(&make_chunk(b"EOF_", &[]));

    let tmp = tempfile::tempdir().unwrap();
    let ctx = ApplyConfig::new(tmp.path()); // no with_observer call
    let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
    ctx.apply_patch(reader).unwrap();
    assert!(
        tmp.path().join("created").is_dir(),
        "ADIR must be applied when no observer is set"
    );
}