zipatch_rs/lib.rs
1//! Parser and applier for FFXIV `ZiPatch` (`.patch`) binary files.
2//!
3//! `zipatch-rs` decodes the binary patch format that Square Enix ships for
4//! Final Fantasy XIV and writes the decoded changes to a local game installation.
5//! The library never touches the network — it operates entirely on byte streams
6//! you supply.
7//!
8//! # Quick start
9//!
10//! The most common usage — apply a single patch file to an install root
11//! with all defaults — is a single call:
12//!
13//! ```no_run
14//! zipatch_rs::apply_patch_file(
15//! "H2017.07.11.0000.0000a.patch",
16//! "/opt/ffxiv/game",
17//! ).unwrap();
18//! ```
19//!
20//! For consumers that need progress observers, custom checkpoint sinks, a
21//! non-default platform, or multi-patch session reuse, use the two-step
22//! form directly:
23//!
24//! ```no_run
25//! use zipatch_rs::{ApplyConfig, open_patch};
26//!
27//! let reader = open_patch("H2017.07.11.0000.0000a.patch").unwrap();
28//! let ctx = ApplyConfig::new("/opt/ffxiv/game");
29//! ctx.apply_patch(reader).unwrap();
30//! ```
31//!
32//! # Choosing an apply pipeline
33//!
34//! `zipatch-rs` ships two apply pipelines. They are not alternatives in the
35//! sense of "one is better" — they answer different questions.
36//!
37//! - **Sequential** ([`ApplyConfig::apply_patch`]) walks a single patch in
38//! chunk order and writes each chunk to disk as it is parsed. One pass,
39//! one patch, bounded memory, minimum latency from "start" to "first byte
40//! on disk". This is the default; reach for it unless you specifically need
41//! what the indexed pipeline offers.
42//! - **Indexed** ([`PlanBuilder`] → [`Plan`] →
43//! [`IndexApplier::execute`](crate::IndexApplier::execute)) folds one or
44//! many patches into a deduplicated, target-major [`Plan`] in memory, then
45//! replays it against the install. The plan is pure data: it can be
46//! inspected, persisted (with the `serde` feature), CRC-populated via
47//! [`Plan::with_crc32`](crate::index::Plan::with_crc32), verified against
48//! the current install via [`PlanVerifier`](crate::index::PlanVerifier),
49//! and used to drive a repair via
50//! [`IndexApplier::execute_with_manifest`](crate::index::IndexApplier::execute_with_manifest)
51//! with a [`RepairManifest`](crate::index::RepairManifest).
52//!
53//! Pick by what the consumer needs:
54//!
55//! | Need | Pipeline |
56//! |---------------------------------------------------------------------|------------|
57//! | Apply one patch to an install, in order, now | Sequential |
58//! | Apply a chain of N patches and dedupe writes superseded mid-chain | Indexed |
59//! | Pre-validate the chain against the install before touching a byte | Indexed |
60//! | Pre-compute expected CRC32s and verify the install against them | Indexed |
61//! | Produce a [`RepairManifest`](crate::index::RepairManifest) of drifted regions | Indexed |
62//! | Persist a structured description of pending work for later replay | Indexed |
63//! | Minimum latency to first write; bounded streaming memory | Sequential |
64//! | Patch source is forward-only (no seeks) | Sequential |
65//!
66//! Costs to weigh: the indexed pipeline does two passes over the patch
67//! source (plan-build, then apply) and holds the plan in memory; the source
68//! must support random access, which is why the indexed entry points take a
69//! [`PatchSource`](crate::index::PatchSource) (typically
70//! [`FilePatchSource`](crate::index::FilePatchSource)) rather than a plain
71//! [`Read`](std::io::Read). The sequential pipeline does a single forward
72//! pass and never holds more than one chunk in flight.
73//!
74//! Starting sequential and growing to indexed later is a supported path —
75//! the [`ApplyConfig`] knobs (`with_observer`, `with_cancel_token`,
76//! `with_checkpoint_sink`, `with_platform`, `with_vfs`) all carry across to
77//! [`IndexApplier`] with the same names and semantics.
78//!
79//! ```no_run
80//! // Sequential: one patch, one pass.
81//! zipatch_rs::apply_patch_file("patch.patch", "/opt/ffxiv/game").unwrap();
82//! ```
83//!
84//! ```no_run
85//! // Indexed: build a plan over a chain, then apply it.
86//! use zipatch_rs::{IndexApplier, PlanBuilder, open_patch};
87//! use zipatch_rs::index::FilePatchSource;
88//!
89//! let mut builder = PlanBuilder::new();
90//! builder.add_patch("p1.patch", open_patch("p1.patch").unwrap()).unwrap();
91//! builder.add_patch("p2.patch", open_patch("p2.patch").unwrap()).unwrap();
92//! let plan = builder.finish();
93//!
94//! let source = FilePatchSource::open_chain(["p1.patch", "p2.patch"]).unwrap();
95//! IndexApplier::new(source, "/opt/ffxiv/game").execute(&plan).unwrap();
96//! ```
97//!
98//! # Inspecting a patch without applying it
99//!
100//! Iterate the reader directly to inspect chunks without touching the
101//! filesystem:
102//!
103//! ```no_run
104//! use zipatch_rs::{Chunk, ZiPatchReader};
105//! use std::fs::File;
106//!
107//! let mut reader = ZiPatchReader::new(File::open("patch.patch").unwrap()).unwrap();
108//! while let Some(rec) = reader.next_chunk().unwrap() {
109//! match rec.chunk {
110//! Chunk::FileHeader(h) => println!("patch version: {:?}", h),
111//! Chunk::AddDirectory(d) => println!("mkdir {}", d.name),
112//! Chunk::Sqpk(cmd) => println!("sqpk: {cmd:?}"),
113//! _ => {}
114//! }
115//! }
116//! ```
117//!
118//! # In-memory doctest
119//!
120//! The following example builds a minimal well-formed patch in memory — magic
121//! header, one `ADIR` chunk (which creates a directory), and an `EOF_`
122//! terminator — then applies it to a temporary directory. This mirrors the
123//! technique used in the crate's own unit tests.
124//!
125//! ```rust
126//! use std::io::Cursor;
127//! use zipatch_rs::{ApplyConfig, Chunk, ZiPatchReader};
128//!
129//! // ZiPatch file magic: \x91ZIPATCH\r\n\x1a\n
130//! const MAGIC: [u8; 12] = [
131//! 0x91, 0x5A, 0x49, 0x50, 0x41, 0x54, 0x43, 0x48,
132//! 0x0D, 0x0A, 0x1A, 0x0A,
133//! ];
134//!
135//! /// Wrap `tag + body` into a length-prefixed, CRC32-verified chunk frame.
136//! fn make_chunk(tag: &[u8; 4], body: &[u8]) -> Vec<u8> {
137//! // CRC is computed over tag ++ body (NOT including the leading body_len).
138//! let mut crc_input = Vec::new();
139//! crc_input.extend_from_slice(tag);
140//! crc_input.extend_from_slice(body);
141//! let crc = crc32fast::hash(&crc_input);
142//!
143//! let mut out = Vec::new();
144//! out.extend_from_slice(&(body.len() as u32).to_be_bytes()); // body_len: u32 BE
145//! out.extend_from_slice(tag); // tag: 4 bytes
146//! out.extend_from_slice(body); // body: body_len bytes
147//! out.extend_from_slice(&crc.to_be_bytes()); // crc32: u32 BE
148//! out
149//! }
150//!
151//! // ADIR body: big-endian u32 name length followed by the name bytes.
152//! let mut adir_body = Vec::new();
153//! adir_body.extend_from_slice(&7u32.to_be_bytes()); // name_len
154//! adir_body.extend_from_slice(b"created"); // name
155//!
156//! // Assemble the full patch stream.
157//! let mut patch = Vec::new();
158//! patch.extend_from_slice(&MAGIC);
159//! patch.extend_from_slice(&make_chunk(b"ADIR", &adir_body));
160//! patch.extend_from_slice(&make_chunk(b"EOF_", &[]));
161//!
162//! // Apply to a temporary directory.
163//! let tmp = tempfile::tempdir().unwrap();
164//! let mut ctx = ApplyConfig::new(tmp.path());
165//! let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
166//! ctx.apply_patch(reader).unwrap();
167//!
168//! assert!(tmp.path().join("created").is_dir());
169//! ```
170//!
171//! # Error handling
172//!
173//! Each layer returns its own domain-specific error type ([`ParseError`],
174//! [`ApplyError`], [`IndexError`], [`VerifyError`]); the corresponding
175//! [`ParseResult`], [`ApplyResult`], [`IndexResult`], and [`VerifyResult`]
176//! aliases at the crate root cover the common signatures.
177//!
178//! Callers that want a single `?`-friendly type at the edges of their code
179//! can use the umbrella [`Error`] enum (and the matching [`Result`] alias) —
180//! every domain error `From`-converts into it, so a one-line `?` collapses
181//! the four domains into one match arm.
182//!
183//! ```no_run
184//! use zipatch_rs::{apply_patch_file, Error, Result};
185//!
186//! fn install(patch: &str, root: &str) -> Result<()> {
187//! apply_patch_file(patch, root)?; // ApplyError -> Error via From
188//! Ok(())
189//! }
190//! # let _ = install;
191//! ```
192//!
193//! # Progress and cancellation
194//!
195//! [`ApplyConfig::with_observer`] installs an [`ApplyObserver`] that is
196//! called after each chunk applies (with the chunk index, tag, and running
197//! byte count from [`chunk::ChunkRecord::bytes_read`]) and polled inside long-
198//! running chunks for cancellation. Returning
199//! [`std::ops::ControlFlow::Break`] from a per-chunk callback, or `true`
200//! from [`ApplyObserver::should_cancel`], aborts the apply call with
201//! [`ApplyError::Cancelled`]. Parsing-only consumers and existing
202//! [`apply_patch`](ApplyConfig::apply_patch) callers that never install an
203//! observer pay nothing — the default is a no-op.
204//!
205//! For cancellation alone, prefer [`CancelToken`]: a cheap cloneable
206//! [`AtomicBool`](std::sync::atomic::AtomicBool) wrapper installed via
207//! [`ApplyConfig::with_cancel_token`] (or
208//! [`IndexApplier::with_cancel_token`](crate::IndexApplier::with_cancel_token)).
209//! Hold a clone on whichever thread initiates cancellation, call
210//! [`CancelToken::cancel`] on a user gesture, and the apply driver picks
211//! up the flip at the next chunk boundary or inner-loop poll point. The
212//! token composes with an installed observer — both are polled, either can
213//! abort.
214//!
215//! # Tracing
216//!
217//! The library emits structured [`tracing`] events and spans across the
218//! parse, plan-build, apply, and verify entry points. Levels follow a
219//! "one event per logical operation at `info!`, per-target/per-fs-op at
220//! `debug!`, per-region/byte-level work at `trace!`" cadence. The top-level
221//! spans listed below are emitted at the `info` level so a subscriber
222//! configured at the default level can scope output via span filtering,
223//! while per-target sub-spans emit at `debug`. Recoverable anomalies — stale
224//! manifest entries, unknown platform IDs, missing-but-ignored files — fire
225//! `warn!`; errors are returned via the domain error types rather than
226//! logged. No subscriber is configured here — that is the consumer's
227//! responsibility.
228//!
229//! [`tracing`]: https://docs.rs/tracing
230//!
231//! ## Stability contract
232//!
233//! `tracing` names are a public API for any consumer wiring up
234//! `tracing-subscriber` filters, OpenTelemetry exporters, log scrapers, or
235//! dashboards that key off span/event/field names. This crate treats them
236//! as such, with the following tiers:
237//!
238//! - **Stable (SemVer-tracked)** — the span names and field names listed in
239//! the catalog below at the **info** and **debug** levels, plus the
240//! per-operation completion-event messages explicitly called out.
241//! Renaming or removing one of these is a minor-version bump while the
242//! crate is on `0.x`/`1.x`; a major-version bump after `2.0`. New spans,
243//! new events, and new fields on existing spans are additive and ship in
244//! patch releases.
245//! - **Best-effort (not contracted)** — `trace!` events and any field they
246//! carry, all event messages not explicitly listed, and the precise
247//! wording of `warn!` messages. These exist for operator forensics and
248//! may change in any release; depend on them only inside a development
249//! environment.
250//!
251//! The canonical strings live in a crate-internal `tracing_schema` module
252//! so a rename lands in a single file. Field names are kept as
253//! literals at the emission sites (their `tracing` macro syntax for
254//! const-named fields would clutter the call site without a corresponding
255//! rename-cost benefit), but they appear in the stable catalog below and
256//! follow the same contract.
257//!
258//! ## Span catalog
259//!
260//! All spans are tagged with the entry point they bracket and the fields
261//! they carry on creation. Sub-spans inherit the parent's field set via
262//! the standard `tracing` span hierarchy.
263//!
264//! ### Info-level (top-level operations)
265//!
266//! | Span | Entry point | Fields |
267//! |---|---|---|
268//! | `apply_patch` | [`ApplyConfig::apply_patch`] | *(none on creation)* |
269//! | `resume_apply_patch` | [`ApplyConfig::resume_apply_patch`] | *(none on creation)* |
270//! | `apply_plan` | [`IndexApplier::execute_with_manifest`](crate::index::IndexApplier::execute_with_manifest) | `mode`, `targets`, `regions` |
271//! | `resume_execute` | [`IndexApplier::execute`](crate::index::IndexApplier::execute) / [`IndexApplier::resume_execute`](crate::index::IndexApplier::resume_execute) | `plan_crc32`, `targets`, `regions`, `fs_ops` |
272//! | `build_plan_patch` | [`PlanBuilder::add_patch`](crate::index::PlanBuilder::add_patch) | `patch` |
273//! | `with_crc32` | [`Plan::with_crc32`](crate::index::Plan::with_crc32) | `targets` |
274//! | `verify_plan` | [`PlanVerifier::execute`](crate::index::PlanVerifier::execute) | `targets` |
275//! | `verify_hashes` | [`HashVerifier::execute`](crate::verify::HashVerifier::execute) | `files` |
276//!
277//! ### Debug-level (per-target / per-file sub-spans)
278//!
279//! | Span | Parent | Fields |
280//! |---|---|---|
281//! | `apply_target` | `apply_plan` / `resume_execute` | `target_idx`, `path`, `regions` |
282//! | `verify_target` | `verify_plan` | `target` |
283//! | `verify_file` | `verify_hashes` | `path` |
284//!
285//! ## Event catalog (info-level, per-operation completion)
286//!
287//! These messages are emitted exactly once per call to their owning entry
288//! point on the success path, and are part of the stable contract:
289//!
290//! | Span | Message | Fields |
291//! |---|---|---|
292//! | `apply_patch` | `apply_patch: patch applied` | `chunks`, `bytes_read`, `resumed_from_chunk`, `elapsed_ms` |
293//! | `resume_apply_patch` | `resume_apply_patch: patch applied` | `chunks`, `bytes_read`, `resumed_from_chunk`, `skipped_bytes` (resumed only), `elapsed_ms` |
294//! | `resume_apply_patch` | `resume_apply_patch: resuming patch` | `patch_name`, `skipped_chunks`, `skipped_bytes`, `has_in_flight` |
295//! | `resume_execute` | `apply_plan: indexed apply complete` | `bytes_written`, `targets`, `resumed_from`, `elapsed_ms` |
296//! | `resume_execute` | `resume_execute: resuming indexed apply` | `plan_crc32`, `skipped_targets`, `skipped_regions`, `fs_ops_skipped` |
297//! | `apply_plan` | `apply_plan: manifest replay complete` | `bytes_written`, `targets`, `regions`, `elapsed_ms` |
298//! | `build_plan_patch` | `plan: patch consumed` | `chunks`, `targets`, `fs_ops` |
299//! | *(none — fires from `PlanBuilder::finish`)* | `plan: built` | `patches`, `targets`, `regions`, `fs_ops` |
300//! | `with_crc32` | `with_crc32: populated CRC32 for regions` | `populated`, `skipped` |
301//! | `verify_plan` | `verify_plan: scan complete` | `targets`, `missing_targets`, `size_mismatched`, `damaged_targets`, `damaged_regions`, `elapsed_ms` |
302//! | `verify_hashes` | `verify_hashes: run complete` | `files`, `failures`, `bytes_hashed`, `elapsed_ms` |
303//!
304//! ## Stable field-name vocabulary
305//!
306//! Field names attached to the spans and events above are stable. Across
307//! the library they're used consistently:
308//!
309//! - `patch`, `patch_name`, `patches`, `chunks`, `bytes_read`, `elapsed_ms`
310//! - `resumed_from_chunk`, `resumed_from`, `skipped_chunks`, `skipped_bytes`,
311//! `skipped_targets`, `skipped_regions`, `has_in_flight`, `fs_ops_skipped`
312//! - `targets`, `regions`, `fs_ops`, `target_idx`, `region_idx`,
313//! `target`, `path`, `mode`
314//! - `plan_crc32`, `populated`, `skipped`, `bytes_written`
315//! - `files`, `failures`, `bytes_hashed`, `missing_targets`,
316//! `size_mismatched`, `damaged_targets`, `damaged_regions`
317//!
318//! Field names emitted only at `debug!` / `trace!` levels (e.g.
319//! `next_chunk_index`, `bytes_into_target`, `block_idx`, `chunk_bytes`,
320//! `delete_zeros`, `decoded_skip`, per-fs-op `folder`/`kind`) are
321//! best-effort and may change without a version bump.
322//!
323//! # API conventions
324//!
325//! All constructors follow consistent naming:
326//!
327//! - **`new(…)`** — canonical constructor; fails only on structurally invalid input
328//! ([`ZiPatchReader::new`] validates the file magic).
329//! - **`open(path)`** — opens a filesystem resource and returns a `Result`
330//! ([`index::FilePatchSource::open`], [`index::FilePatchSource::open_chain`]).
331//! - **`with_X(self, value) -> Self`** — builder-style setter; chains on
332//! [`ApplyConfig`] and [`IndexApplier`].
333//! - **`into_X(self)`** — type transition consuming `self`
334//! ([`ApplyConfig::into_session`]).
335//! - **`execute(self, …) -> Result<T>`** — runs a pipeline to completion
336//! ([`IndexApplier::execute`](crate::IndexApplier::execute),
337//! [`index::PlanVerifier::execute`],
338//! [`verify::HashVerifier::execute`]).
339//! - **`finish(self) -> T`** — builder finaliser with no I/O
340//! ([`index::PlanBuilder::finish`]).
341//!
342//! The crate root re-exports the happy-path symbols. Secondary types live in
343//! their owning modules:
344//!
345//! - [`chunk`] — [`chunk::ChunkRecord`], [`chunk::SqpkCommand`],
346//! [`chunk::SqpackFileId`], [`chunk::SqpkCompressedBlock`]
347//! - [`apply`] — [`apply::CheckpointSink`], [`apply::Checkpoint`],
348//! [`apply::CheckpointPolicy`], [`apply::SequentialCheckpoint`],
349//! [`apply::IndexedCheckpoint`], [`apply::NoopCheckpointSink`],
350//! [`apply::NoopObserver`], [`apply::Vfs`], [`apply::StdFs`],
351//! [`apply::InMemoryFs`]
352//! - [`index`] — [`index::PlanVerifier`], [`index::PatchSource`],
353//! [`index::FilePatchSource`], [`index::RepairManifest`]
354//! - [`newtypes`] — [`newtypes::PatchIndex`], [`newtypes::ChunkTag`],
355//! [`newtypes::SchemaVersion`]
356//!
357//! # Async usage
358//!
359//! `zipatch-rs` is a **synchronous** crate. Every public trait
360//! ([`index::PatchSource`], [`apply::CheckpointSink`],
361//! [`ApplyObserver`], [`apply::Vfs`]) and every driver entry point
362//! ([`ApplyConfig::apply_patch`], [`IndexApplier::execute`](crate::index::IndexApplier::execute),
363//! [`ZiPatchReader::next_chunk`]) is blocking. The crate has no
364//! runtime dependency.
365//!
366//! This is a deliberate design choice. The apply-side hot path is
367//! dominated by DEFLATE decompression (CPU-bound) and filesystem
368//! syscalls (blocking by their nature on every mainstream OS); neither
369//! benefits from an `async` signature. Staying sync also keeps the crate
370//! trivially embeddable in a CLI binary, a Tauri command, or an
371//! async-runtime worker without forcing a runtime choice or pulling
372//! `tokio` into the dependency graph.
373//!
374//! ## Driving the crate from an async runtime
375//!
376//! Async consumers (e.g. a tokio-based launcher) park the whole apply
377//! call on a blocking-pool thread:
378//!
379//! ```ignore
380//! // pseudo-code; the crate has no tokio dependency
381//! let install_path = install_path.clone();
382//! let patch_path = patch_path.clone();
383//! let cancel = zipatch_rs::CancelToken::new();
384//! let cancel_bg = cancel.clone();
385//!
386//! // ... wire `cancel.cancel()` to a tokio::select! arm or UI handler ...
387//!
388//! let result = tokio::task::spawn_blocking(move || {
389//! let reader = zipatch_rs::open_patch(&patch_path)?;
390//! let ctx = zipatch_rs::ApplyConfig::new(install_path)
391//! .with_cancel_token(cancel_bg);
392//! ctx.apply_patch(reader)
393//! }).await??;
394//! ```
395//!
396//! Per-chunk progress reaches the async UI through an
397//! [`mpsc`](std::sync::mpsc) (or tokio-mpsc-equivalent) channel whose
398//! `Sender` lives inside an [`ApplyObserver`] impl and whose `Receiver`
399//! is polled from an async task. Consumers that need both progress and
400//! cancellation pair an observer (for progress) with a [`CancelToken`]
401//! (for cancellation) — they compose without a wrapper observer.
402//!
403//! ## Async-backed sources
404//!
405//! Implementors whose backing storage is itself async (e.g. an
406//! in-progress download, an async KV store for checkpoints, a remote
407//! object-store [`apply::Vfs`]) satisfy the sync traits by bridging through a
408//! channel against a separate async task: the sync method blocks on a
409//! response from the async side. Because the trait methods run from
410//! inside the `spawn_blocking` thread, blocking on the channel does
411//! not stall the runtime's reactor.
412
413#![deny(missing_docs)]
414#![cfg_attr(docsrs, feature(doc_cfg))]
415
416#[cfg(not(any(feature = "rust-backend", feature = "zlib-rs", feature = "zlib-ng")))]
417compile_error!(
418 "zipatch-rs requires a flate2 backend. Enable exactly one of the features \
419 `rust-backend` (default, pure-Rust via miniz_oxide), `zlib-rs` (pure-Rust \
420 via zlib-rs), or `zlib-ng` (C via zlib-ng). If you set \
421 `default-features = false`, re-enable a backend explicitly, e.g. \
422 `features = [\"rust-backend\"]`."
423);
424
425/// Filesystem application of parsed chunks ([`ApplyConfig`]).
426pub mod apply;
427/// Wire-format chunk types and the [`ZiPatchReader`] iterator.
428pub mod chunk;
429/// Domain-split error types ([`ParseError`], [`ApplyError`], [`IndexError`], [`VerifyError`]).
430pub mod error;
431/// Indexed-apply plan model and single-patch builder
432/// ([`Plan`], [`PlanBuilder`]).
433pub mod index;
434/// Strongly-typed wrappers around primitive identifiers in the public API
435/// ([`newtypes::PatchIndex`], [`newtypes::ChunkTag`], [`newtypes::SchemaVersion`]).
436pub mod newtypes;
437pub(crate) mod reader;
438pub(crate) mod tracing_schema;
439/// Post-apply integrity verification against caller-supplied file hashes.
440pub mod verify;
441
442/// Command-line inspector scaffolding for the `zipatch` binary.
443///
444/// Gated behind the `cli` cargo feature so the default library build pulls in
445/// no extra dependencies. The binary entry point lives at `src/bin/zipatch.rs`.
446#[cfg(feature = "cli")]
447#[cfg_attr(docsrs, doc(cfg(feature = "cli")))]
448pub mod cli;
449
450/// Test fixtures quarantined behind the `test-utils` Cargo feature.
451///
452/// `#[doc(hidden)]` so this module — and everything it re-exports — stays
453/// out of the user-facing rustdoc landing page. See the module rustdoc for
454/// the no-stability stance.
455#[cfg(any(test, feature = "test-utils"))]
456#[doc(hidden)]
457pub mod test_utils;
458
459/// Fuzz-only re-exports of crate-internal primitives.
460///
461/// `cfg(fuzzing)` is set automatically by cargo-fuzz when compiling a fuzz
462/// target — it is never set in normal `cargo build` / `cargo test` / CI builds.
463/// Nothing exported from this module is part of the public API.
464#[cfg(fuzzing)]
465#[doc(hidden)]
466pub mod fuzz_internal {
467 pub use crate::reader::ReadExt;
468}
469
470// =============================================================================
471// Public API — curated crate-root surface
472//
473// Only the happy-path symbols are re-exported here. Everything else stays
474// addressable via its module path (`zipatch_rs::chunk::ChunkRecord`,
475// `zipatch_rs::apply::CheckpointSink`, `zipatch_rs::newtypes::PatchIndex`,
476// `zipatch_rs::index::PlanVerifier`, …) but does not crowd the landing page.
477// =============================================================================
478
479// --- One-shot drivers — the 90% case ---
480pub use chunk::open_patch;
481// `apply_patch_file` is defined below.
482
483// --- Parsing primary types ---
484pub use chunk::{Chunk, ZiPatchReader};
485
486// --- Apply primary types ---
487pub use apply::{ApplyConfig, ApplyObserver, ApplySession, CancelToken, ChunkEvent};
488
489// --- Indexed-pipeline primary types ---
490pub use index::{IndexApplier, Plan, PlanBuilder};
491
492// --- Central domain types ---
493// `Platform` is defined at the crate root (below) since it has no natural home
494// in a sub-module — every layer references it.
495
496// --- Error umbrella + per-domain errors and result aliases ---
497pub use error::{
498 ApplyError, ApplyResult, Error, IndexError, IndexResult, ParseError, ParseResult, Result,
499 VerifyError, VerifyResult,
500};
501
502/// One-shot: open `patch_path` and apply every chunk to `install_root` with
503/// all defaults.
504///
505/// Equivalent to:
506///
507/// ```ignore
508/// let reader = zipatch_rs::open_patch(patch_path)?;
509/// zipatch_rs::ApplyConfig::new(install_root).apply_patch(reader)?;
510/// ```
511///
512/// The returned [`ApplyResult`] surfaces parsing failures as
513/// [`ApplyError::Parse`] and apply-side failures as their respective
514/// [`ApplyError`] variants, so a single match arm covers both layers.
515///
516/// Use the two-step form ([`open_patch`] + [`ApplyConfig::apply_patch`])
517/// when you need to install an [`ApplyObserver`], a [`apply::CheckpointSink`],
518/// a custom [`apply::Vfs`], a non-default [`Platform`], or to reuse one
519/// [`ApplySession`] across multiple patches.
520///
521/// # Errors
522///
523/// Returns the first failure surfaced by either layer:
524///
525/// - [`ApplyError::Parse`] wrapping a [`ParseError`] for header, chunk, or
526/// stream-shape problems (including the initial file open).
527/// - [`ApplyError::Io`] for filesystem failures against `install_root`.
528/// - [`ApplyError::UnsupportedPlatform`] if the patch declares a platform
529/// this build does not know how to resolve `SqPack` paths for.
530///
531/// # Example
532///
533/// ```no_run
534/// zipatch_rs::apply_patch_file(
535/// "H2017.07.11.0000.0000a.patch",
536/// "/opt/ffxiv/game",
537/// ).unwrap();
538/// ```
539pub fn apply_patch_file(
540 patch_path: impl AsRef<std::path::Path>,
541 install_root: impl AsRef<std::path::Path>,
542) -> ApplyResult<()> {
543 let reader = open_patch(patch_path)?;
544 ApplyConfig::new(install_root.as_ref().to_path_buf()).apply_patch(reader)
545}
546
547/// Target platform for `SqPack` file path resolution.
548///
549/// FFXIV's `SqPack` archive files live in platform-specific subdirectories
550/// under the game install root. For example, a data file for the Windows
551/// client lives at `sqpack/ffxiv/000000.win32.dat0`, while the PS4 equivalent
552/// is `sqpack/ffxiv/000000.ps4.dat0`. The [`Platform`] value stored in an
553/// [`ApplyConfig`] selects which suffix is used when resolving chunk targets
554/// to filesystem paths.
555///
556/// # Default
557///
558/// An [`ApplyConfig`] defaults to [`Platform::Win32`]. Override this at
559/// construction time with [`ApplyConfig::with_platform`].
560///
561/// # Runtime override via `SqpkTargetInfo`
562///
563/// In practice, real FFXIV patch files begin with an `SQPK T` chunk
564/// (`SqpkTargetInfo`) that declares the target platform. When
565/// [`Chunk::apply`] is called on that chunk, it overwrites
566/// [`ApplyConfig::platform`] with the decoded [`Platform`] value. This means
567/// the default is only relevant for synthetic patches or when you know the
568/// target in advance and want to assert it before the stream starts.
569///
570/// # Forward compatibility
571///
572/// The enum is `#[non_exhaustive]` because Square Enix has shipped console
573/// targets on a sliding schedule (Win32, PS3, PS4) and a future PS5 / Switch
574/// build would land here as a new named variant. The [`Platform::Unknown`]
575/// variant preserves unrecognised platform IDs so that newer patch files do
576/// not fail parsing when a new platform is introduced. Path resolution for `SqPack`
577/// `.dat`/`.index` files refuses to guess and returns
578/// [`ApplyError::UnsupportedPlatform`] carrying the raw `platform_id` —
579/// silently substituting a default layout would risk writing platform-specific
580/// data to the wrong file.
581///
582/// # Display
583///
584/// Implements [`std::fmt::Display`]: `"Win32"`, `"PS3"`, `"PS4"`, or
585/// `"Unknown(N)"` where `N` is the raw platform ID.
586///
587/// # Example
588///
589/// ```rust
590/// use zipatch_rs::{ApplyConfig, Platform};
591///
592/// let ctx = ApplyConfig::new("/opt/ffxiv/game")
593/// .with_platform(Platform::Win32);
594///
595/// assert_eq!(ctx.platform(), Platform::Win32);
596/// assert_eq!(format!("{}", Platform::Unknown(99)), "Unknown(99)");
597/// ```
598///
599#[non_exhaustive]
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
602pub enum Platform {
603 /// Windows / PC client (`win32` path suffix).
604 ///
605 /// This is the platform used by all current PC releases of FFXIV and is
606 /// the default for [`ApplyConfig`].
607 Win32,
608 /// `PlayStation` 3 client (`ps3` path suffix).
609 ///
610 /// PS3 support was discontinued after FFXIV: A Realm Reborn. Patches
611 /// targeting this platform are no longer issued by Square Enix, but the
612 /// variant is retained for completeness.
613 Ps3,
614 /// `PlayStation` 4 client (`ps4` path suffix).
615 ///
616 /// Active platform alongside Windows. PS4 patches share the same chunk
617 /// structure as Windows patches but target different file paths.
618 Ps4,
619 /// Unrecognised platform ID preserved from a `SqpkTargetInfo` chunk.
620 ///
621 /// When the apply layer encounters a `platform_id` it does not recognise,
622 /// it stores the raw `u16` value here and emits a `warn!` tracing event.
623 /// Subsequent `SqPack` path resolution returns
624 /// [`ApplyError::UnsupportedPlatform`] carrying the same `u16` rather
625 /// than silently routing writes to a default layout — quietly substituting
626 /// `win32` paths for an unknown platform would corrupt the on-disk install
627 /// with platform-specific data written to the wrong files. Non-SqPack
628 /// chunks (e.g. `ADIR`, `DELD`, or `SqpkFile` operations resolved via a
629 /// generic path) continue to apply, so an unknown platform only aborts at
630 /// the first `.dat` or `.index` lookup.
631 Unknown(u16),
632}
633
634impl std::fmt::Display for Platform {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 match self {
637 Platform::Win32 => f.write_str("Win32"),
638 Platform::Ps3 => f.write_str("PS3"),
639 Platform::Ps4 => f.write_str("PS4"),
640 Platform::Unknown(id) => write!(f, "Unknown({id})"),
641 }
642 }
643}