zipatch-rs 1.1.0

Parser for FFXIV ZiPatch patch files
Documentation
//! Shared fixtures for the crate's own tests.
//!
//! Gated behind the `test-utils` feature flag. The contents of this module
//! are **not part of the stable public API** — they exist so that unit tests
//! inside this crate and integration tests in `tests/` can share the same
//! chunk-framing boilerplate without re-implementing it per file. Anything
//! exposed here can change between minor versions without notice.
//!
//! External consumers writing their own integration tests against synthetic
//! patches may opt into this module by enabling the `test-utils` feature in
//! their `Cargo.toml`, but should understand they are pinning to an internal
//! testing aid rather than a versioned public API.

/// The 12-byte `ZiPatch` file magic: `\x91ZIPATCH\r\n\x1a\n`.
///
/// Every well-formed `ZiPatch` stream begins with these exact bytes;
/// [`crate::ZiPatchReader::new`] validates them on construction.
pub const MAGIC: [u8; 12] = [
    0x91, 0x5A, 0x49, 0x50, 0x41, 0x54, 0x43, 0x48, 0x0D, 0x0A, 0x1A, 0x0A,
];

/// Wrap `tag` + `body` into a well-formed chunk frame.
///
/// The on-disk layout is `[body_len: u32 BE | tag: 4 | body | CRC32: u32 BE]`,
/// where the CRC32 is computed over `tag ++ body` (the leading length is *not*
/// included). This matches the wire format consumed by
/// [`crate::ZiPatchReader`].
///
/// # Panics
///
/// Panics if `body.len()` does not fit in a `u32`. The on-wire length field is
/// 32 bits and the parser already rejects oversized chunks, so this is a
/// genuine programmer error rather than a runtime concern.
#[must_use]
pub fn make_chunk(tag: &[u8; 4], body: &[u8]) -> Vec<u8> {
    let mut crc_input = Vec::with_capacity(4 + body.len());
    crc_input.extend_from_slice(tag);
    crc_input.extend_from_slice(body);
    let crc = crc32fast::hash(&crc_input);

    let mut out = Vec::with_capacity(4 + 4 + body.len() + 4);
    let body_len = u32::try_from(body.len()).expect("chunk body fits in u32");
    out.extend_from_slice(&body_len.to_be_bytes());
    out.extend_from_slice(tag);
    out.extend_from_slice(body);
    out.extend_from_slice(&crc.to_be_bytes());
    out
}

/// Assemble a complete patch stream from a sequence of pre-framed chunks.
///
/// Prepends [`MAGIC`] to the concatenation of `chunks`. Callers are responsible
/// for terminating the stream with an `EOF_` chunk if [`crate::ZiPatchReader`]
/// is to see the patch as complete.
#[must_use]
pub fn make_patch(chunks: &[Vec<u8>]) -> Vec<u8> {
    let total: usize = MAGIC.len() + chunks.iter().map(Vec::len).sum::<usize>();
    let mut out = Vec::with_capacity(total);
    out.extend_from_slice(&MAGIC);
    for chunk in chunks {
        out.extend_from_slice(chunk);
    }
    out
}