Skip to main content

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 tracing_schema;
438/// Post-apply integrity verification against caller-supplied file hashes.
439pub mod verify;
440
441/// Test fixtures quarantined behind the `test-utils` Cargo feature.
442///
443/// `#[doc(hidden)]` so this module — and everything it re-exports — stays
444/// out of the user-facing rustdoc landing page. See the module rustdoc for
445/// the no-stability stance.
446#[cfg(any(test, feature = "test-utils"))]
447#[doc(hidden)]
448pub mod test_utils;
449
450// =============================================================================
451// Public API — curated crate-root surface
452//
453// Only the happy-path symbols are re-exported here. Everything else stays
454// addressable via its module path (`zipatch_rs::chunk::ChunkRecord`,
455// `zipatch_rs::apply::CheckpointSink`, `zipatch_rs::newtypes::PatchIndex`,
456// `zipatch_rs::index::PlanVerifier`, …) but does not crowd the landing page.
457// =============================================================================
458
459// --- One-shot drivers — the 90% case ---
460pub use chunk::open_patch;
461// `apply_patch_file` is defined below.
462
463// --- Parsing primary types ---
464pub use chunk::{Chunk, ZiPatchReader};
465
466// --- Apply primary types ---
467pub use apply::{ApplyConfig, ApplyObserver, ApplySession, CancelToken, ChunkEvent};
468
469// --- Indexed-pipeline primary types ---
470pub use index::{IndexApplier, Plan, PlanBuilder};
471
472// --- Central domain types ---
473// `Platform` is defined at the crate root (below) since it has no natural home
474// in a sub-module — every layer references it.
475
476// --- Error umbrella + per-domain errors and result aliases ---
477pub use error::{
478    ApplyError, ApplyResult, Error, IndexError, IndexResult, ParseError, ParseResult, Result,
479    VerifyError, VerifyResult,
480};
481
482/// One-shot: open `patch_path` and apply every chunk to `install_root` with
483/// all defaults.
484///
485/// Equivalent to:
486///
487/// ```ignore
488/// let reader = zipatch_rs::open_patch(patch_path)?;
489/// zipatch_rs::ApplyConfig::new(install_root).apply_patch(reader)?;
490/// ```
491///
492/// The returned [`ApplyResult`] surfaces parsing failures as
493/// [`ApplyError::Parse`] and apply-side failures as their respective
494/// [`ApplyError`] variants, so a single match arm covers both layers.
495///
496/// Use the two-step form ([`open_patch`] + [`ApplyConfig::apply_patch`])
497/// when you need to install an [`ApplyObserver`], a [`apply::CheckpointSink`],
498/// a custom [`apply::Vfs`], a non-default [`Platform`], or to reuse one
499/// [`ApplySession`] across multiple patches.
500///
501/// # Errors
502///
503/// Returns the first failure surfaced by either layer:
504///
505/// - [`ApplyError::Parse`] wrapping a [`ParseError`] for header, chunk, or
506///   stream-shape problems (including the initial file open).
507/// - [`ApplyError::Io`] for filesystem failures against `install_root`.
508/// - [`ApplyError::UnsupportedPlatform`] if the patch declares a platform
509///   this build does not know how to resolve `SqPack` paths for.
510///
511/// # Example
512///
513/// ```no_run
514/// zipatch_rs::apply_patch_file(
515///     "H2017.07.11.0000.0000a.patch",
516///     "/opt/ffxiv/game",
517/// ).unwrap();
518/// ```
519pub fn apply_patch_file(
520    patch_path: impl AsRef<std::path::Path>,
521    install_root: impl AsRef<std::path::Path>,
522) -> ApplyResult<()> {
523    let reader = open_patch(patch_path)?;
524    ApplyConfig::new(install_root.as_ref().to_path_buf()).apply_patch(reader)
525}
526
527/// Target platform for `SqPack` file path resolution.
528///
529/// FFXIV's `SqPack` archive files live in platform-specific subdirectories
530/// under the game install root. For example, a data file for the Windows
531/// client lives at `sqpack/ffxiv/000000.win32.dat0`, while the PS4 equivalent
532/// is `sqpack/ffxiv/000000.ps4.dat0`. The [`Platform`] value stored in an
533/// [`ApplyConfig`] selects which suffix is used when resolving chunk targets
534/// to filesystem paths.
535///
536/// # Default
537///
538/// An [`ApplyConfig`] defaults to [`Platform::Win32`]. Override this at
539/// construction time with [`ApplyConfig::with_platform`].
540///
541/// # Runtime override via `SqpkTargetInfo`
542///
543/// In practice, real FFXIV patch files begin with an `SQPK T` chunk
544/// (`SqpkTargetInfo`) that declares the target platform. When
545/// [`Chunk::apply`] is called on that chunk, it overwrites
546/// [`ApplyConfig::platform`] with the decoded [`Platform`] value. This means
547/// the default is only relevant for synthetic patches or when you know the
548/// target in advance and want to assert it before the stream starts.
549///
550/// # Forward compatibility
551///
552/// The enum is `#[non_exhaustive]` because Square Enix has shipped console
553/// targets on a sliding schedule (Win32, PS3, PS4) and a future PS5 / Switch
554/// build would land here as a new named variant. The [`Platform::Unknown`]
555/// variant preserves unrecognised platform IDs so that newer patch files do
556/// not fail parsing when a new platform is introduced. Path resolution for `SqPack`
557/// `.dat`/`.index` files refuses to guess and returns
558/// [`ApplyError::UnsupportedPlatform`] carrying the raw `platform_id` —
559/// silently substituting a default layout would risk writing platform-specific
560/// data to the wrong file.
561///
562/// # Display
563///
564/// Implements [`std::fmt::Display`]: `"Win32"`, `"PS3"`, `"PS4"`, or
565/// `"Unknown(N)"` where `N` is the raw platform ID.
566///
567/// # Example
568///
569/// ```rust
570/// use zipatch_rs::{ApplyConfig, Platform};
571///
572/// let ctx = ApplyConfig::new("/opt/ffxiv/game")
573///     .with_platform(Platform::Win32);
574///
575/// assert_eq!(ctx.platform(), Platform::Win32);
576/// assert_eq!(format!("{}", Platform::Unknown(99)), "Unknown(99)");
577/// ```
578///
579#[non_exhaustive]
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
582pub enum Platform {
583    /// Windows / PC client (`win32` path suffix).
584    ///
585    /// This is the platform used by all current PC releases of FFXIV and is
586    /// the default for [`ApplyConfig`].
587    Win32,
588    /// `PlayStation` 3 client (`ps3` path suffix).
589    ///
590    /// PS3 support was discontinued after FFXIV: A Realm Reborn. Patches
591    /// targeting this platform are no longer issued by Square Enix, but the
592    /// variant is retained for completeness.
593    Ps3,
594    /// `PlayStation` 4 client (`ps4` path suffix).
595    ///
596    /// Active platform alongside Windows. PS4 patches share the same chunk
597    /// structure as Windows patches but target different file paths.
598    Ps4,
599    /// Unrecognised platform ID preserved from a `SqpkTargetInfo` chunk.
600    ///
601    /// When the apply layer encounters a `platform_id` it does not recognise,
602    /// it stores the raw `u16` value here and emits a `warn!` tracing event.
603    /// Subsequent `SqPack` path resolution returns
604    /// [`ApplyError::UnsupportedPlatform`] carrying the same `u16` rather
605    /// than silently routing writes to a default layout — quietly substituting
606    /// `win32` paths for an unknown platform would corrupt the on-disk install
607    /// with platform-specific data written to the wrong files. Non-SqPack
608    /// chunks (e.g. `ADIR`, `DELD`, or `SqpkFile` operations resolved via a
609    /// generic path) continue to apply, so an unknown platform only aborts at
610    /// the first `.dat` or `.index` lookup.
611    Unknown(u16),
612}
613
614impl std::fmt::Display for Platform {
615    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
616        match self {
617            Platform::Win32 => f.write_str("Win32"),
618            Platform::Ps3 => f.write_str("PS3"),
619            Platform::Ps4 => f.write_str("PS4"),
620            Platform::Unknown(id) => write!(f, "Unknown({id})"),
621        }
622    }
623}