Skip to main content

Crate zipatch_rs

Crate zipatch_rs 

Source
Expand description

Parser and applier for FFXIV ZiPatch (.patch) binary files.

zipatch-rs decodes the binary patch format that Square Enix ships for Final Fantasy XIV and writes the decoded changes to a local game installation. The library never touches the network — it operates entirely on byte streams you supply.

§Architecture

The crate is split into three layers that share types but are otherwise independent:

§Layer 1 — I/O primitives (reader)

reader::ReadExt is a crate-internal extension trait that adds typed big- and little-endian reads on top of std::io::Read. It is not part of the public API; the parsing layer uses it exclusively.

§Layer 2 — Parsing (chunk)

ZiPatchReader is an Iterator over Chunk values. Construct it from any std::io::Read source (a std::fs::File, a std::io::Cursor<Vec<u8>>, a network stream, …). It validates the 12-byte file magic on construction, then yields one Chunk per Iterator::next call until it sees the EOF_ terminator or hits an error.

Nothing in the parsing layer allocates file handles, stats paths, or performs I/O against the install tree. Parse-only users can consume ZiPatchReader without ever importing apply.

§Layer 3 — Applying (apply)

The Apply trait bridges parsing and application: every Chunk variant implements it, and each implementation writes the patch change to disk via an ApplyContext. ApplyContext holds the install root, the target Platform, behavioural flags, and an internal file-handle cache that avoids re-opening the same .dat file for every chunk.

§Quick start

The most common usage: open a patch file, build a context, apply every chunk in stream order.

use std::fs::File;
use zipatch_rs::{ApplyContext, ZiPatchReader};

let patch_file = File::open("H2017.07.11.0000.0000a.patch").unwrap();
let mut ctx = ApplyContext::new("/opt/ffxiv/game");

ZiPatchReader::new(patch_file)
    .unwrap()
    .apply_to(&mut ctx)
    .unwrap();

§Inspecting a patch without applying it

Iterate the reader directly to inspect chunks without touching the filesystem:

use zipatch_rs::{Chunk, ZiPatchReader};
use std::fs::File;

let reader = ZiPatchReader::new(File::open("patch.patch").unwrap()).unwrap();
for chunk in reader {
    match chunk.unwrap() {
        Chunk::FileHeader(h) => println!("patch version: {:?}", h),
        Chunk::AddDirectory(d) => println!("mkdir {}", d.name),
        Chunk::Sqpk(cmd) => println!("sqpk: {cmd:?}"),
        _ => {}
    }
}

§In-memory doctest

The following example builds a minimal well-formed patch in memory — magic header, one ADIR chunk (which creates a directory), and an EOF_ terminator — then applies it to a temporary directory. This mirrors the technique used in the crate’s own unit tests.

use std::io::Cursor;
use zipatch_rs::{ApplyContext, Chunk, ZiPatchReader};

// ZiPatch file magic: \x91ZIPATCH\r\n\x1a\n
const MAGIC: [u8; 12] = [
    0x91, 0x5A, 0x49, 0x50, 0x41, 0x54, 0x43, 0x48,
    0x0D, 0x0A, 0x1A, 0x0A,
];

/// Wrap `tag + body` into a length-prefixed, CRC32-verified chunk frame.
fn make_chunk(tag: &[u8; 4], body: &[u8]) -> Vec<u8> {
    // CRC is computed over tag ++ body (NOT including the leading body_len).
    let mut crc_input = Vec::new();
    crc_input.extend_from_slice(tag);
    crc_input.extend_from_slice(body);
    let crc = crc32fast::hash(&crc_input);

    let mut out = Vec::new();
    out.extend_from_slice(&(body.len() as u32).to_be_bytes()); // body_len: u32 BE
    out.extend_from_slice(tag);                                // tag: 4 bytes
    out.extend_from_slice(body);                               // body: body_len bytes
    out.extend_from_slice(&crc.to_be_bytes());                 // crc32: u32 BE
    out
}

// ADIR body: big-endian u32 name length followed by the name bytes.
let mut adir_body = Vec::new();
adir_body.extend_from_slice(&7u32.to_be_bytes()); // name_len
adir_body.extend_from_slice(b"created");          // name

// Assemble the full patch stream.
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_", &[]));

// Apply to a temporary directory.
let tmp = tempfile::tempdir().unwrap();
let mut ctx = ApplyContext::new(tmp.path());
ZiPatchReader::new(Cursor::new(patch))
    .unwrap()
    .apply_to(&mut ctx)
    .unwrap();

assert!(tmp.path().join("created").is_dir());

§Error handling

Every fallible operation returns Result<T>, which is an alias for std::result::Result<T, ZiPatchError>. Parse errors and apply errors share the same type so callers need only one error arm.

§Progress and cancellation

ApplyContext::with_observer installs an ApplyObserver that is called after each chunk applies (with the chunk index, tag, and running byte count from ZiPatchReader::bytes_read) and polled inside long- running chunks for cancellation. Returning std::ops::ControlFlow::Break from a per-chunk callback, or true from ApplyObserver::should_cancel, aborts the apply call with ZiPatchError::Cancelled. Parsing-only consumers and existing apply_to callers that never install an observer pay nothing — the default is a no-op.

§Tracing

The library emits structured tracing events and spans across the parse, plan-build, apply, and verify entry points. Levels follow a “one event per logical operation at info!, per-target/per-fs-op at debug!, per-region/byte-level work at trace!” cadence. The top-level spans (apply_patch, apply_plan, build_plan_patch, compute_crc32, verify_plan) are emitted at the info level so a subscriber configured at the default level can scope output via span filtering, while per-target sub-spans (apply_target) emit at debug. Recoverable anomalies — stale manifest entries, unknown platform IDs, missing-but-ignored files — fire warn!; errors are returned via ZiPatchError rather than logged. No subscriber is configured here — that is the consumer’s responsibility.

Re-exports§

pub use apply::Apply;
pub use apply::ApplyContext;
pub use apply::ApplyObserver;
pub use apply::Checkpoint;
pub use apply::CheckpointPolicy;
pub use apply::CheckpointSink;
pub use apply::ChunkEvent;
pub use apply::InFlightAddFile;
pub use apply::IndexedCheckpoint;
pub use apply::NoopCheckpointSink;
pub use apply::NoopObserver;
pub use apply::SequentialCheckpoint;
pub use chunk::Chunk;
pub use chunk::ZiPatchReader;
pub use error::ZiPatchError;
pub use index::IndexApplier;
pub use index::Plan;
pub use index::PlanBuilder;
pub use index::Verifier;

Modules§

apply
Filesystem application of parsed chunks (Apply, ApplyContext). Filesystem application of parsed ZiPatch chunks.
chunk
Wire-format chunk types and the ZiPatchReader iterator. Wire-format chunk types and the ZiPatchReader iterator.
error
Error type returned by parsing and applying (ZiPatchError).
index
Indexed-apply plan model and single-patch builder (Plan, PlanBuilder). Pure-data indexed-apply plan model, single-patch builder, applier, and verifier.

Enums§

Platform
Target platform for SqPack file path resolution.

Type Aliases§

Result
Crate-wide Result alias parameterised over ZiPatchError.