# 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.