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.

§Tracing

The library emits structured tracing events at trace!, debug!, and warn! levels. No subscriber is configured here — configure output in your application binary (or in gaveloc’s launcher binary).

Re-exports§

pub use apply::Apply;
pub use apply::ApplyContext;
pub use chunk::Chunk;
pub use chunk::ZiPatchReader;
pub use error::ZiPatchError;

Modules§

apply
Filesystem application of parsed chunks (Apply, ApplyContext). Filesystem application of parsed ZiPatch chunks.
chunk
Wire-format chunk types and the ZiPatchReader iterator.
error
Error type returned by parsing and applying (ZiPatchError).

Enums§

Platform
Target platform for SqPack file path resolution.

Type Aliases§

Result
Crate-wide Result alias parameterised over ZiPatchError.