zipatch-rs 1.6.0

Parser for FFXIV ZiPatch patch files
Documentation
//! Test fixtures. **Not part of the stable API; semver does not apply.**
//!
//! Enabled by the `test-utils` Cargo feature for downstream test crates that
//! need to construct synthetic patches or inspect internal counters. The
//! contents of this module — chunk-framing helpers, wire-format type
//! re-exports under `wire`, an in-memory [`PatchSource`](crate::index::PatchSource)
//! implementation, and any test-only accessors on production structs — exist
//! solely to keep this crate's own integration tests honest without
//! duplicating boilerplate per file. Fields, items, and the module itself may
//! change, be renamed, or vanish between minor releases without notice.
//!
//! Reach for this module only from `#[cfg(test)]` code or from a downstream
//! integration-test crate that opts into the `test-utils` feature with full
//! awareness of the no-stability stance.

/// Wire-format chunk body types, re-exported for synthetic-chunk construction
/// in tests.
///
/// **Not part of the stable public API.** These are the `binrw`-derived
/// structs that parse SE's `.patch` wire format. Their fields, layout, and
/// existence may change without a semver bump. External code should match on
/// [`Chunk`](crate::Chunk) and [`SqpkCommand`](crate::chunk::SqpkCommand) for
/// inspection rather than naming these types.
#[doc(hidden)]
pub mod wire {
    pub use crate::chunk::adir::AddDirectory;
    pub use crate::chunk::afsp::ApplyFreeSpace;
    pub use crate::chunk::aply::{ApplyOption, ApplyOptionKind};
    pub use crate::chunk::ddir::DeleteDirectory;
    pub use crate::chunk::fhdr::{FileHeader, FileHeaderV2, FileHeaderV3};
    pub use crate::chunk::sqpk::add_data::SqpkAddData;
    pub use crate::chunk::sqpk::delete_data::SqpkDeleteData;
    pub use crate::chunk::sqpk::expand_data::SqpkExpandData;
    pub use crate::chunk::sqpk::file::{SqpkFile, SqpkFileOperation};
    pub use crate::chunk::sqpk::header::{
        SqpkHeader, SqpkHeaderTarget, TargetFileKind, TargetHeaderKind,
    };
    pub use crate::chunk::sqpk::index::{IndexCommand, SqpkIndex, SqpkPatchInfo};
    pub use crate::chunk::sqpk::target_info::SqpkTargetInfo;
}

#[doc(hidden)]
pub use crate::index::source::MemoryPatchSource;

/// 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
}