wow-mpq 0.6.4

High-performance parser for World of Warcraft MPQ archives with parallel processing support
Documentation
use wow_mpq::Archive;
/**
 * MPQ Patch File Integration Tests
 *
 * These tests verify patch file support using real Cataclysm patch data.
 * Reference implementation: StormLib
 *
 * Test data generated by: tests/stormlib_patch_test.cpp
 * - stormlib_base_file.bin: Base M2 model (78,784 bytes)
 * - stormlib_patched_result.bin: Final patched M2 model (92,448 bytes)
 */
use wow_mpq::patch::{PatchFile, PatchType};

const UPDATE_MPQ1: &str =
    "/home/danielsreichenbach/Downloads/wow/4.3.4/4.3.4/Data/wow-update-base-15211.MPQ";

const TEST_FILE: &str = "Item/ObjectComponents/Head/Helm_Robe_RaidWarlock_F_01_WoF.M2";

/// Test that we can identify patch files in update MPQs
#[test]
#[ignore = "Requires MPQ test files not available in CI"]
fn test_identify_patch_file() {
    // Open the update MPQ directly (it contains patch files)
    let archive = Archive::open(UPDATE_MPQ1).expect("Failed to open update MPQ");

    // Find the file in the update MPQ
    let file_info = archive
        .find_file(TEST_FILE)
        .expect("Failed to find file")
        .expect("File not found");

    println!("File flags: 0x{:08X}", file_info.flags);
    println!("Is patch file: {}", file_info.is_patch_file());

    // This file should have the PATCH_FILE flag in the update MPQ
    assert!(
        file_info.is_patch_file(),
        "Expected file to be a patch file"
    );
}

/// Test PTCH header parsing with real patch data
#[test]
#[ignore] // Will work after implementing patch file extraction
fn test_parse_patch_header_real_data() {
    // This test will be enabled once we can extract raw patch file data
    // For now, it's a placeholder showing the intended test structure

    let mut archive = Archive::open(UPDATE_MPQ1).expect("Failed to open update MPQ");

    // Try to read the raw patch file (this will currently fail with OperationNotSupported)
    let result = archive.read_file(TEST_FILE);

    match result {
        Err(wow_mpq::Error::OperationNotSupported { .. }) => {
            println!("Patch file correctly rejected (as expected before implementation)");
        }
        Ok(_) => panic!("Should not be able to read patch file directly"),
        Err(e) => panic!("Unexpected error: {e}"),
    }
}

/// Test parsing a minimal valid PTCH header (synthetic data)
#[test]
fn test_parse_ptch_header_synthetic() {
    // Create a minimal valid PTCH file structure
    let mut data = Vec::new();

    // PTCH Header (16 bytes)
    data.extend_from_slice(&0x48435450u32.to_le_bytes()); // 'PTCH' signature
    data.extend_from_slice(&1000u32.to_le_bytes()); // patch_data_size
    data.extend_from_slice(&5000u32.to_le_bytes()); // size_before
    data.extend_from_slice(&6000u32.to_le_bytes()); // size_after

    // MD5 Block (40 bytes)
    data.extend_from_slice(&0x5f35444du32.to_le_bytes()); // 'MD5_' signature
    data.extend_from_slice(&40u32.to_le_bytes()); // md5_block_size
    data.extend_from_slice(&[0x11u8; 16]); // md5_before (dummy)
    data.extend_from_slice(&[0x22u8; 16]); // md5_after (dummy)

    // XFRM Block Header (12 bytes)
    data.extend_from_slice(&0x4d524658u32.to_le_bytes()); // 'XFRM' signature
    data.extend_from_slice(&112u32.to_le_bytes()); // xfrm_block_size (includes header)
    data.extend_from_slice(&0x59504f43u32.to_le_bytes()); // 'COPY' patch type

    // Add some dummy patch data (100 bytes to match xfrm_block_size - 12)
    data.extend_from_slice(&[0x42u8; 100]);

    // Parse the header
    let patch = PatchFile::parse(&data).expect("Failed to parse PTCH file");

    assert_eq!(patch.header.patch_data_size, 1000);
    assert_eq!(patch.header.size_before, 5000);
    assert_eq!(patch.header.size_after, 6000);
    assert_eq!(patch.header.md5_before, [0x11u8; 16]);
    assert_eq!(patch.header.md5_after, [0x22u8; 16]);
    assert_eq!(patch.header.patch_type, PatchType::Copy);
    assert_eq!(patch.header.xfrm_data_size, 100);
    assert_eq!(patch.data.len(), 100);
}

/// Test parsing BSD0 patch type
#[test]
fn test_parse_bsd0_patch_type() {
    let mut data = Vec::new();

    // PTCH Header
    data.extend_from_slice(&0x48435450u32.to_le_bytes());
    data.extend_from_slice(&2000u32.to_le_bytes());
    data.extend_from_slice(&10000u32.to_le_bytes());
    data.extend_from_slice(&12000u32.to_le_bytes());

    // MD5 Block
    data.extend_from_slice(&0x5f35444du32.to_le_bytes());
    data.extend_from_slice(&40u32.to_le_bytes());
    data.extend_from_slice(&[0x33u8; 16]);
    data.extend_from_slice(&[0x44u8; 16]);

    // XFRM Block with BSD0 type
    data.extend_from_slice(&0x4d524658u32.to_le_bytes());
    data.extend_from_slice(&212u32.to_le_bytes());
    data.extend_from_slice(&0x30445342u32.to_le_bytes()); // 'BSD0' patch type

    // Add dummy bsdiff data
    data.extend_from_slice(&[0x99u8; 200]);

    let patch = PatchFile::parse(&data).expect("Failed to parse BSD0 patch");

    assert_eq!(patch.header.patch_type, PatchType::Bsd0);
    assert_eq!(patch.data.len(), 200);
}

/// Test invalid PTCH signature
#[test]
fn test_invalid_ptch_signature() {
    let mut data = Vec::new();
    data.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Invalid signature
    data.extend_from_slice(&[0u8; 60]); // Rest of header

    let result = PatchFile::parse(&data);
    assert!(result.is_err());
}

/// Test invalid MD5 signature
#[test]
fn test_invalid_md5_signature() {
    let mut data = Vec::new();

    // Valid PTCH header
    data.extend_from_slice(&0x48435450u32.to_le_bytes());
    data.extend_from_slice(&1000u32.to_le_bytes());
    data.extend_from_slice(&5000u32.to_le_bytes());
    data.extend_from_slice(&6000u32.to_le_bytes());

    // Invalid MD5 signature
    data.extend_from_slice(&0xBADBADBAu32.to_le_bytes());
    data.extend_from_slice(&[0u8; 40]);

    let result = PatchFile::parse(&data);
    assert!(result.is_err());
}

/// Test invalid XFRM signature
#[test]
fn test_invalid_xfrm_signature() {
    let mut data = Vec::new();

    // Valid PTCH header
    data.extend_from_slice(&0x48435450u32.to_le_bytes());
    data.extend_from_slice(&1000u32.to_le_bytes());
    data.extend_from_slice(&5000u32.to_le_bytes());
    data.extend_from_slice(&6000u32.to_le_bytes());

    // Valid MD5 block
    data.extend_from_slice(&0x5f35444du32.to_le_bytes());
    data.extend_from_slice(&40u32.to_le_bytes());
    data.extend_from_slice(&[0x11u8; 16]);
    data.extend_from_slice(&[0x22u8; 16]);

    // Invalid XFRM signature
    data.extend_from_slice(&0xBADBADBAu32.to_le_bytes());
    data.extend_from_slice(&[0u8; 8]);

    let result = PatchFile::parse(&data);
    assert!(result.is_err());
}

/// Test unknown patch type
#[test]
fn test_unknown_patch_type() {
    let mut data = Vec::new();

    // Valid PTCH header
    data.extend_from_slice(&0x48435450u32.to_le_bytes());
    data.extend_from_slice(&1000u32.to_le_bytes());
    data.extend_from_slice(&5000u32.to_le_bytes());
    data.extend_from_slice(&6000u32.to_le_bytes());

    // Valid MD5 block
    data.extend_from_slice(&0x5f35444du32.to_le_bytes());
    data.extend_from_slice(&40u32.to_le_bytes());
    data.extend_from_slice(&[0x11u8; 16]);
    data.extend_from_slice(&[0x22u8; 16]);

    // XFRM block with unknown patch type
    data.extend_from_slice(&0x4d524658u32.to_le_bytes());
    data.extend_from_slice(&112u32.to_le_bytes());
    data.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown patch type

    let result = PatchFile::parse(&data);
    assert!(result.is_err());
}

/// Test file too small to contain full header
#[test]
fn test_file_too_small() {
    let data = vec![0u8; 32]; // Only 32 bytes, need 64

    let result = PatchFile::parse(&data);
    assert!(result.is_err());
}

/// Test MD5 verification methods (unit test style)
#[test]
fn test_md5_verification() {
    use md5::{Digest, Md5};

    // Create test data
    let base_data = b"Hello, World!";
    let patched_data = b"Hello, Warcraft!";

    // Calculate MD5 hashes
    let mut hasher = Md5::new();
    hasher.update(base_data);
    let md5_before: [u8; 16] = hasher.finalize().into();

    let mut hasher = Md5::new();
    hasher.update(patched_data);
    let md5_after: [u8; 16] = hasher.finalize().into();

    // Create a synthetic patch file
    let mut data = Vec::new();
    data.extend_from_slice(&0x48435450u32.to_le_bytes());
    data.extend_from_slice(&100u32.to_le_bytes());
    data.extend_from_slice(&(base_data.len() as u32).to_le_bytes());
    data.extend_from_slice(&(patched_data.len() as u32).to_le_bytes());

    data.extend_from_slice(&0x5f35444du32.to_le_bytes());
    data.extend_from_slice(&40u32.to_le_bytes());
    data.extend_from_slice(&md5_before);
    data.extend_from_slice(&md5_after);

    data.extend_from_slice(&0x4d524658u32.to_le_bytes());
    data.extend_from_slice(&112u32.to_le_bytes());
    data.extend_from_slice(&0x59504f43u32.to_le_bytes());
    data.extend_from_slice(&[0u8; 100]);

    let patch = PatchFile::parse(&data).expect("Failed to parse patch");

    // Test verification methods
    assert!(patch.verify_base(base_data).is_ok());
    assert!(patch.verify_base(b"Wrong data").is_err());

    assert!(patch.verify_patched(patched_data).is_ok());
    assert!(patch.verify_patched(b"Wrong data").is_err());
}