zipatch-rs 1.5.0

Parser for FFXIV ZiPatch patch files
Documentation
//! CPU-bound benchmark for `ZiPatchReader` chunk parsing.
//!
//! Builds a synthetic in-memory patch once outside the timed region and
//! measures the cost of iterating it to completion. The patch mixes several
//! chunk tags (FHDR, APLY, ADIR, DDIR, four SQPK sub-commands, EOF_) to
//! exercise the dispatcher rather than a single hot path. CRC32 verification
//! is left enabled so the bench reflects the default `apply_to` workload.
//!
//! Run with `cargo bench --bench parse --all-features`.

use std::hint::black_box;
use std::io::Cursor;

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use zipatch_rs::ZiPatchReader;
use zipatch_rs::test_utils::{make_chunk, make_patch};

/// How many copies of the mixed-chunk block to repeat in each synthetic patch.
///
/// One iteration of `mixed_chunks` produces eight non-EOF chunks; the patch
/// frames a multiple of that, terminated by a single EOF_. The sweep covers
/// small, medium, and large in-memory streams without slipping into
/// allocator-noise territory.
const REPEATS: &[usize] = &[1, 16, 256];

/// Build one round of mixed chunk frames covering the dispatch arms most
/// commonly seen in real patch files (FHDR/APLY/SQPK) plus the rarer
/// directory-management tags (ADIR/DDIR).
fn mixed_chunks() -> Vec<Vec<u8>> {
    vec![
        make_chunk(b"FHDR", &fhdr_v2_body()),
        make_chunk(b"APLY", &aply_body(1, false)),
        make_chunk(b"ADIR", &dir_body(b"sqpack/ffxiv")),
        make_chunk(b"DELD", &dir_body(b"sqpack/ex1")),
        make_chunk(b"SQPK", &sqpk_target_info_body()),
        make_chunk(b"SQPK", &sqpk_delete_data_body()),
        make_chunk(b"SQPK", &sqpk_expand_data_body()),
        make_chunk(b"SQPK", &sqpk_file_makedir_body(b"sqpack/ex2")),
    ]
}

/// FHDR v2 body. The 32-bit version word is little-endian; the v2 fields are
/// big-endian. The trailing 8 zero bytes are bounded by the body slice and
/// ignored by the parser.
fn fhdr_v2_body() -> Vec<u8> {
    let mut out = Vec::with_capacity(20);
    // version = 2 lives in bits 16..23 of a little-endian u32: 0x0002_0000.
    out.extend_from_slice(&0x0002_0000u32.to_le_bytes());
    out.extend_from_slice(b"D000"); // patch_type
    out.extend_from_slice(&1u32.to_be_bytes()); // entry_files
    out.extend_from_slice(&[0u8; 8]); // trailing zeros — ignored
    out
}

/// APLY body: `[kind: u32 BE] [pad: 4] [value: u32 BE, non-zero = true]`.
fn aply_body(kind: u32, value: bool) -> Vec<u8> {
    let mut out = Vec::with_capacity(12);
    out.extend_from_slice(&kind.to_be_bytes());
    out.extend_from_slice(&[0u8; 4]);
    out.extend_from_slice(&u32::from(value).to_be_bytes());
    out
}

/// Shared `[name_len: u32 BE] [name]` body used by both ADIR and DELD.
fn dir_body(name: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(4 + name.len());
    let name_len = u32::try_from(name.len()).expect("bench name fits in u32");
    out.extend_from_slice(&name_len.to_be_bytes());
    out.extend_from_slice(name);
    out
}

/// SQPK chunk body wrapping a `T` (target info) sub-command.
fn sqpk_target_info_body() -> Vec<u8> {
    let mut cmd = Vec::with_capacity(27);
    cmd.extend_from_slice(&[0u8; 3]); // reserved
    cmd.extend_from_slice(&0u16.to_be_bytes()); // platform_id = Win32
    cmd.extend_from_slice(&(-1i16).to_be_bytes()); // region = Global
    cmd.extend_from_slice(&0i16.to_be_bytes()); // is_debug = false
    cmd.extend_from_slice(&0u16.to_be_bytes()); // version
    cmd.extend_from_slice(&0u64.to_le_bytes()); // deleted_data_size (LE)
    cmd.extend_from_slice(&0u64.to_le_bytes()); // seek_count (LE)
    wrap_sqpk(b'T', &cmd)
}

/// SQPK `D` (delete data) sub-command body.
fn sqpk_delete_data_body() -> Vec<u8> {
    let mut cmd = Vec::with_capacity(23);
    cmd.extend_from_slice(&[0u8; 3]); // alignment
    cmd.extend_from_slice(&0u16.to_be_bytes()); // main_id
    cmd.extend_from_slice(&0u16.to_be_bytes()); // sub_id
    cmd.extend_from_slice(&0u32.to_be_bytes()); // file_id
    cmd.extend_from_slice(&0u32.to_be_bytes()); // block_offset_raw
    cmd.extend_from_slice(&1u32.to_be_bytes()); // block_count
    cmd.extend_from_slice(&[0u8; 4]); // reserved
    wrap_sqpk(b'D', &cmd)
}

/// SQPK `E` (expand data) sub-command body — same layout as `D`.
fn sqpk_expand_data_body() -> Vec<u8> {
    let mut cmd = Vec::with_capacity(23);
    cmd.extend_from_slice(&[0u8; 3]);
    cmd.extend_from_slice(&0u16.to_be_bytes());
    cmd.extend_from_slice(&0u16.to_be_bytes());
    cmd.extend_from_slice(&0u32.to_be_bytes());
    cmd.extend_from_slice(&0u32.to_be_bytes());
    cmd.extend_from_slice(&1u32.to_be_bytes());
    cmd.extend_from_slice(&[0u8; 4]);
    wrap_sqpk(b'E', &cmd)
}

/// SQPK `F` (file) sub-command with operation `M` (make-dir-tree) — no inline
/// block payloads, so the body stays small and the parser stays on the
/// dispatch path rather than block decoding.
fn sqpk_file_makedir_body(path: &[u8]) -> Vec<u8> {
    let mut path_bytes = path.to_vec();
    path_bytes.push(0); // NUL terminator
    let path_len = u32::try_from(path_bytes.len()).expect("bench path fits in u32");

    let mut cmd = Vec::with_capacity(27 + path_bytes.len());
    cmd.push(b'M'); // operation = MakeDirTree
    cmd.extend_from_slice(&[0u8; 2]); // alignment
    cmd.extend_from_slice(&0u64.to_be_bytes()); // file_offset
    cmd.extend_from_slice(&0u64.to_be_bytes()); // file_size
    cmd.extend_from_slice(&path_len.to_be_bytes());
    cmd.extend_from_slice(&0u16.to_be_bytes()); // expansion_id
    cmd.extend_from_slice(&[0u8; 2]); // padding
    cmd.extend_from_slice(&path_bytes);
    wrap_sqpk(b'F', &cmd)
}

/// Frame an SQPK sub-command into the outer chunk body: 4-byte `inner_size`
/// BE prefix + 1-byte command tag + sub-command bytes.
fn wrap_sqpk(command: u8, cmd_body: &[u8]) -> Vec<u8> {
    let inner_size = 5 + cmd_body.len();
    let inner_size_u32 = u32::try_from(inner_size).expect("bench SQPK body fits in u32");
    let mut out = Vec::with_capacity(inner_size);
    out.extend_from_slice(&inner_size_u32.to_be_bytes());
    out.push(command);
    out.extend_from_slice(cmd_body);
    out
}

/// Assemble `repeat` copies of the mixed chunk block, terminated by `EOF_`.
fn build_patch(repeat: usize) -> Vec<u8> {
    let one_round = mixed_chunks();
    let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(one_round.len() * repeat + 1);
    for _ in 0..repeat {
        chunks.extend(one_round.iter().cloned());
    }
    chunks.push(make_chunk(b"EOF_", &[]));
    make_patch(&chunks)
}

fn bench_parse(c: &mut Criterion) {
    let mut group = c.benchmark_group("parse/mixed");
    for &repeat in REPEATS {
        let patch = build_patch(repeat);
        group.throughput(Throughput::Bytes(patch.len() as u64));
        group.bench_with_input(BenchmarkId::from_parameter(repeat), &patch, |b, patch| {
            b.iter(|| {
                let reader =
                    ZiPatchReader::new(Cursor::new(patch.as_slice())).expect("magic is valid");
                let mut count = 0usize;
                for chunk in reader {
                    let chunk = chunk.expect("synthetic patch parses cleanly");
                    black_box(&chunk);
                    count += 1;
                }
                black_box(count);
            });
        });
    }
    group.finish();
}

criterion_group!(benches, bench_parse);
criterion_main!(benches);