zipatch-rs 1.6.0

Parser for FFXIV ZiPatch patch files
Documentation
# Architecture

Internal design notes for contributors and maintainers. None of this is load-bearing for users of the crate — see [README.md](README.md) and [docs.rs](https://docs.rs/zipatch-rs) for the public API.

## Three-layer design

The crate is organised in three layers with a strict one-way dependency: each layer may only import from the layer(s) below it.

```
Layer 3  src/apply/         — ApplyConfig / ApplySession / IndexApplier
Layer 2  src/chunk/         — Chunk enum, ZiPatchReader, SqpkCommand
Layer 1  src/reader.rs      — ReadExt (big-endian primitives over std::io::Read)
```

**Layer 1 — `src/reader.rs`**

`ReadExt` is a thin trait over `std::io::Read` that adds typed big-endian read helpers (`read_u8`, `read_u16_be`, `read_u32_be`, etc.). All chunk parsers use it. No allocations beyond fixed-size arrays; no seeking.

**Layer 2 — `src/chunk/`**

Houses the `Chunk` enum and its sub-types (`SqpkCommand`, `SqpkFile`, etc.), plus `ZiPatchReader` — the streaming iterator that parses the binary wire format frame by frame. The parser has no knowledge of the filesystem, no file handles, and performs no I/O against the install tree. Every type in this layer is pure data.

The sole entry point into layer 2 from the outside is `ZiPatchReader::next_chunk()`, which returns `Option<ChunkRecord<Chunk>>`. `ChunkRecord` carries the parsed chunk plus byte-accounting fields.

Wire format per chunk:

```
[body_len: u32 BE][tag: 4 bytes][body: body_len bytes][crc32: u32 BE]
```

CRC32 is computed over `tag ++ body` (not over `body_len`). The parser verifies CRC32 on every chunk before returning it to the caller.

**Layer 3 — `src/apply/`**

The apply layer sits on top of the parser and adds the two capabilities the parser intentionally omits: filesystem I/O (via the `Vfs` trait) and DEFLATE decompression (via `flate2`). `Chunk::apply(&mut ApplySession)` is the bridge method defined in `src/apply/mod.rs` that dispatches each chunk variant to its apply-side logic.

The apply layer is itself split into two complementary types:

- `ApplyConfig` — frozen configuration (install root, platform, `Vfs` backing, observer, checkpoint sink). Performs no I/O. Constructed on the caller's thread and shipped to a worker thread for the actual apply.
- `ApplySession` — runtime state (open file-handle cache, path caches, reusable DEFLATE decompressor, per-chunk progress counters). Created by consuming an `ApplyConfig` via `into_session()`.

## Module map

```
src/
├── lib.rs               — crate root, re-exports, Platform enum, apply_patch_file
├── reader.rs            — ReadExt trait (pub(crate))
├── error.rs             — ParseError, ApplyError, IndexError, VerifyError, Error umbrella
├── newtypes.rs          — PatchIndex, ChunkTag, SchemaVersion
├── tracing_schema.rs    — stable span/event name constants (pub(crate))
├── test_utils.rs        — test fixtures (test-utils feature, doc(hidden))
│
├── chunk/               — Layer 2: wire-format parsing
│   ├── mod.rs           — Chunk enum, ChunkRecord, ZiPatchReader, open_patch
│   ├── adir.rs          — AddDirectory chunk
│   ├── afsp.rs          — ApplyFreeSpace chunk (no-op at apply time)
│   ├── aply.rs          — ApplyOption chunk (sets ignore flags)
│   ├── ddir.rs          — DeleteDirectory chunk
│   ├── fhdr.rs          — FileHeader chunk
│   ├── util.rs          — SqpackFileId, SqpkCompressedBlock helpers
│   └── sqpk/            — SQPK sub-commands
│       ├── mod.rs       — SqpkCommand enum
│       ├── add_data.rs  — SqpkAddData (A)
│       ├── delete_data.rs — SqpkDeleteData (D)
│       ├── expand_data.rs — SqpkExpandData (E)
│       ├── header.rs    — SqpkHeader (H)
│       ├── index.rs     — SqpkIndex (I, no-op)
│       ├── sqpk_file.rs — SqpkFile (F) — AddFile, DeleteFile, RemoveAll, MakeDirTree
│       └── target_info.rs — SqpkTargetInfo (T)
│
├── apply/               — Layer 3: filesystem application
│   ├── mod.rs           — ApplyConfig, ApplySession, Chunk::apply dispatch
│   ├── cancel.rs        — CancelToken
│   ├── checkpoint.rs    — Checkpoint, CheckpointPolicy, CheckpointSink, SequentialCheckpoint, IndexedCheckpoint
│   ├── driver.rs        — sequential apply loop (apply_patch, resume_apply_patch)
│   ├── observer.rs      — ApplyObserver, ChunkEvent, NoopObserver
│   ├── path.rs          — SqPack path resolution (pub(crate))
│   ├── sqpk.rs          — SQPK apply logic (pub(crate))
│   └── vfs.rs           — Vfs trait, StdFs, InMemoryFs
│
├── index/               — indexed pipeline
│   ├── mod.rs           — public re-exports
│   ├── plan.rs          — Plan, Target, Region, PartSource, PartExpected, FilesystemOp, TargetPath
│   ├── builder.rs       — PlanBuilder
│   ├── apply.rs         — IndexApplier
│   ├── source.rs        — PatchSource trait, FilePatchSource
│   ├── verify.rs        — PlanVerifier, RepairManifest
│   └── region_map.rs    — per-target region accumulator (pub(crate))
│
├── verify/
│   └── mod.rs           — HashVerifier, ExpectedHash, FileVerifyOutcome, VerifyOutcome
│
└── cli/                 — cli feature only
    ├── mod.rs           — Cli, Commands, run()
    └── dump.rs          — dump subcommand implementation
```

## Ecosystem context

`zipatch-rs` and `sqpack-rs` are standalone libraries that exist independently of the launcher product. They have no network dependency and no runtime requirements beyond Rust's standard library plus the dependencies listed in `Cargo.toml`.

The launcher product that consumes `zipatch-rs` is a separate workspace. It handles patch discovery, authentication, download, and UI; `zipatch-rs` handles only the binary format parsing and application once bytes are on disk.

## Key design decisions

**Parse/apply separation**: Nothing in `src/chunk/` touches the filesystem. This means the parser is testable with byte buffers and the apply layer is testable against an `InMemoryFs` without any real files. It also means consumers that only need to inspect patch contents (e.g. the `zipatch dump` CLI) pay zero apply-layer overhead.

**`Vfs` abstraction**: All filesystem effects are routed through a `Vfs` trait rather than `std::fs` directly. The `InMemoryFs` implementation makes apply-layer testing fast and hermetic. The synchronous trait surface was a deliberate choice: the apply hot path is dominated by DEFLATE decompression and blocking syscalls, both of which are fundamentally synchronous, and keeping the trait sync avoids pulling a runtime into the dependency graph.

**`ApplyConfig` / `ApplySession` split**: `ApplyConfig` is the builder; `ApplySession` holds the runtime state. This split enables the common "construct on the UI thread, ship to a worker" pattern without requiring `Arc<Mutex<_>>` on the session's mutable state.

**Error domain split**: `ParseError`, `ApplyError`, `IndexError`, and `VerifyError` each cover their own domain rather than collapsing into one enum. This lets callers pattern-match at the appropriate granularity. The umbrella `Error` enum with `From` impls for each domain type provides a single `?`-friendly exit point for callers who don't need the distinction.

**`#[non_exhaustive]` on structs and enums**: All public plan-model types (`Plan`, `Target`, `Region`, `PartSource`, `PartExpected`, `FilesystemOp`, checkpoint types) are `#[non_exhaustive]`. This preserves the ability to add fields (e.g. per-region provenance metadata) in future minor versions without a breaking change, at the cost of requiring `::new()` constructors for external callers who want to construct them.

**Tracing stability contract**: Span and field names at `info!` and `debug!` levels are treated as a public API. They are kept in a single `tracing_schema` module so a rename lands in one file. `trace!` names are best-effort. See the "Tracing" section of the crate-level rustdoc for the full catalog.