zipatch_rs/error.rs
1//! Error types for parsing, applying, indexing, and verifying ZiPatch streams.
2//!
3//! The crate splits failures along the four real domain boundaries rather
4//! than presenting a single flat enum:
5//!
6//! - [`ParseError`] — surfaced by the parsing layer ([`crate::chunk`] and the
7//! [`crate::ZiPatchReader`] streaming parser). Pure wire-format concerns: bad
8//! magic, unknown tags, CRC mismatches, truncated streams, field invariants.
9//! - [`ApplyError`] — surfaced by the apply layer ([`crate::apply`]). Wraps
10//! [`ParseError`] because the sequential apply driver pulls chunks from a
11//! [`crate::ZiPatchReader`] internally, and adds apply-side concerns: I/O,
12//! platform resolution, cancellation, DEFLATE failures.
13//! - [`IndexError`] — surfaced by the indexed pipeline ([`crate::index`]).
14//! Wraps [`ParseError`] (plan building parses chunks) and carries
15//! index-only concerns: plan-source mismatches, schema versions, unsafe
16//! target paths, duplicate-patch chains.
17//! - [`VerifyError`] — surfaced by the verifier ([`crate::verify`] and
18//! [`crate::index::PlanVerifier`]). Plan-shape invariants and on-disk content
19//! mismatches.
20//!
21//! [`Error`] is a top-level umbrella that all four `From`-convert into, for
22//! callers that prefer to handle every failure mode behind one enum.
23//!
24//! # Third-party error types
25//!
26//! The library does not expose [`binrw::Error`] in its public surface — a
27//! `binrw` parse failure is wrapped as [`ParseError::Decode`], which carries
28//! the field context and message as owned strings. The original
29//! [`binrw::Error`] is retained internally but not reachable through
30//! [`std::error::Error::source`].
31//!
32//! [`std::io::Error`] is kept as a `#[source]` on the variants that genuinely
33//! wrap one (file open/seek/write failures, parser stream reads), because it
34//! is a standard-library type and callers expect it for I/O operations.
35//! Where the apply or verify layer has the path that failed, the variant
36//! carries an optional [`std::path::PathBuf`] alongside the error so the
37//! consumer does not have to reconstruct it.
38
39use crate::newtypes::{ChunkTag, PatchIndex, SchemaVersion};
40use std::path::PathBuf;
41
42// ---------------------------------------------------------------------------
43// ParseError — wire-format / streaming-decode failures
44// ---------------------------------------------------------------------------
45
46/// Failures produced by the parsing layer ([`crate::chunk`]).
47///
48/// Surfaced by [`crate::ZiPatchReader::new`], [`crate::open_patch`], and
49/// [`crate::ZiPatchReader::next_chunk`]. None of these variants involve the
50/// filesystem of the install target — they describe shapes the parser
51/// rejected.
52///
53/// `#[non_exhaustive]`: parse-error vocabulary grows as the parser surfaces
54/// new structural failure modes.
55#[non_exhaustive]
56#[derive(Debug, thiserror::Error)]
57pub enum ParseError {
58 /// I/O failure reading from the patch source.
59 #[error("I/O error reading patch stream: {source}")]
60 Io {
61 /// Underlying [`std::io::Error`] from the patch source.
62 #[source]
63 source: std::io::Error,
64 },
65
66 /// The 12-byte `ZiPatch` magic header was missing or did not match.
67 #[error("invalid magic bytes")]
68 InvalidMagic,
69
70 /// A 4-byte chunk tag was not recognised by the parser.
71 #[error("unknown chunk tag: {0}")]
72 UnknownChunkTag(ChunkTag),
73
74 /// A SQPK sub-command byte was not recognised by the parser.
75 #[error("unknown SQPK command: {0:#x}")]
76 UnknownSqpkCommand(u8),
77
78 /// A chunk's recorded CRC32 did not match the computed CRC32.
79 #[error("CRC32 mismatch on chunk {tag}: expected {expected:#010x}, got {actual:#010x}")]
80 ChecksumMismatch {
81 /// Tag of the chunk whose checksum failed.
82 tag: ChunkTag,
83 /// CRC32 stored in the patch file.
84 expected: u32,
85 /// CRC32 computed over the actual chunk bytes.
86 actual: u32,
87 },
88
89 /// A field value failed a parser invariant (e.g. negative size).
90 #[error("invalid field: {context}")]
91 InvalidField {
92 /// Human-readable description of which field was invalid and why.
93 context: &'static str,
94 },
95
96 /// A chunk declared a size larger than the parser's maximum (512 MiB).
97 #[error("chunk size {0} exceeds maximum")]
98 OversizedChunk(usize),
99
100 /// A `SqpkFile` operation byte was not recognised.
101 #[error("unknown SqpkFile operation: {0:#02x}")]
102 UnknownFileOperation(u8),
103
104 /// A UTF-8 decode failed when reading a path or name field.
105 #[error("UTF-8 decode error: {0}")]
106 Utf8Error(#[from] std::string::FromUtf8Error),
107
108 /// DEFLATE decompression of a `SqpkCompressedBlock` payload failed.
109 ///
110 /// Surfaced both by the standalone
111 /// [`SqpkCompressedBlock::decompress_into`](crate::chunk::SqpkCompressedBlock::decompress_into)
112 /// helper (a parse-side API on the chunk type) and propagated up through
113 /// apply when the apply layer drives block decompression.
114 #[error("decompression error: {source}")]
115 Decompress {
116 /// Underlying [`std::io::Error`] returned by the decompressor.
117 #[source]
118 source: std::io::Error,
119 },
120
121 /// A binary decode (via the underlying `binrw` parser) failed.
122 ///
123 /// The third-party error type is **not** exposed in this variant — only
124 /// the field context (where parsing failed) and the formatted message
125 /// (what went wrong) are surfaced. Callers should treat this as an
126 /// opaque "the wire format was malformed at this field" signal.
127 #[error("decode error in {context}: {message}")]
128 Decode {
129 /// Static description of which field or chunk failed to decode.
130 context: &'static str,
131 /// Formatted message from the underlying decoder.
132 message: String,
133 },
134
135 /// A `SqpkFile` carried a negative `file_offset` that cannot be applied.
136 #[error("negative file_offset in SqpkFile: {0}")]
137 NegativeFileOffset(i64),
138
139 /// Stream ended without an `EOF_` chunk; download or copy was truncated.
140 #[error("patch file ended without EOF_ chunk (truncated download?)")]
141 TruncatedPatch,
142}
143
144impl From<std::io::Error> for ParseError {
145 fn from(source: std::io::Error) -> Self {
146 ParseError::Io { source }
147 }
148}
149
150impl From<binrw::Error> for ParseError {
151 fn from(err: binrw::Error) -> Self {
152 ParseError::Decode {
153 context: "binrw",
154 message: err.to_string(),
155 }
156 }
157}
158
159//---------------------------------------------------------------------------
160// ApplyError — apply-layer failures
161// ---------------------------------------------------------------------------
162
163/// Failures produced by the apply layer ([`crate::apply`]).
164///
165/// Wraps [`ParseError`] because the sequential apply driver
166/// ([`crate::ApplyConfig::apply_patch`]) parses chunks as it applies them.
167///
168/// `#[non_exhaustive]`: apply-side failure modes (new I/O kinds surfaced
169/// as their own variants, new cancellation reasons) accumulate over time.
170#[non_exhaustive]
171#[derive(Debug, thiserror::Error)]
172pub enum ApplyError {
173 /// A parse failure surfaced while the apply driver was pulling chunks.
174 #[error(transparent)]
175 Parse(#[from] ParseError),
176
177 /// An I/O operation against the install root failed.
178 ///
179 /// `path` carries the file or directory the operation was targeting when
180 /// it has been threaded through; it is `None` for operations whose path
181 /// context is not readily available (e.g. raw `?` propagation from a
182 /// helper that does not know the path).
183 #[error("I/O error{}: {source}", path.as_ref().map(|p| format!(" at {}", p.display())).unwrap_or_default())]
184 Io {
185 /// Path the failing operation was targeting, if known.
186 path: Option<PathBuf>,
187 /// Underlying [`std::io::Error`].
188 #[source]
189 source: std::io::Error,
190 },
191
192 /// `SqPack` path resolution refused to fall back to a default platform layout.
193 #[error("unsupported platform id {0}: cannot resolve SqPack path")]
194 UnsupportedPlatform(u16),
195
196 /// Apply was cancelled by an [`crate::ApplyObserver`].
197 #[error("apply cancelled by observer")]
198 Cancelled,
199
200 /// A persisted record's `schema_version` does not match this build.
201 #[error("{kind} schema version mismatch: persisted {found}, this build expects {expected}")]
202 SchemaVersionMismatch {
203 /// Type name whose schema the mismatch refers to.
204 kind: &'static str,
205 /// Persisted version.
206 found: SchemaVersion,
207 /// Version this build supports.
208 expected: SchemaVersion,
209 },
210}
211
212impl From<std::io::Error> for ApplyError {
213 fn from(source: std::io::Error) -> Self {
214 ApplyError::Io { path: None, source }
215 }
216}
217
218impl ApplyError {
219 /// Construct an [`ApplyError::Io`] tagged with the path the operation
220 /// was targeting.
221 #[must_use]
222 pub fn io_at(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
223 ApplyError::Io {
224 path: Some(path.into()),
225 source,
226 }
227 }
228}
229
230// ---------------------------------------------------------------------------
231// IndexError — indexed-pipeline failures
232// ---------------------------------------------------------------------------
233
234/// Failures produced by the indexed pipeline ([`crate::index`]).
235///
236/// `#[non_exhaustive]`: the indexed pipeline is the youngest layer and
237/// has the highest expected churn in surfaced failure modes.
238#[non_exhaustive]
239#[derive(Debug, thiserror::Error)]
240pub enum IndexError {
241 /// A parse failure surfaced while the plan builder was scanning chunks.
242 #[error(transparent)]
243 Parse(#[from] ParseError),
244
245 /// An I/O failure during plan building, source reads, or indexed apply.
246 #[error("I/O error{}: {source}", path.as_ref().map(|p| format!(" at {}", p.display())).unwrap_or_default())]
247 Io {
248 /// Path the failing operation was targeting, if known.
249 path: Option<PathBuf>,
250 /// Underlying [`std::io::Error`].
251 #[source]
252 source: std::io::Error,
253 },
254
255 /// `SqPack` path resolution refused to fall back to a default platform layout.
256 #[error("unsupported platform id {0}: cannot resolve SqPack path")]
257 UnsupportedPlatform(u16),
258
259 /// Indexed apply was cancelled by an observer or checkpoint signal.
260 #[error("apply cancelled")]
261 Cancelled,
262
263 /// An indexed-apply plan referenced a region whose source bytes are not
264 /// reachable from the current applier.
265 #[error(
266 "indexed apply: source bytes unavailable for region at target_offset {target_offset} length {length}"
267 )]
268 SourceUnavailable {
269 /// Target-file offset of the unavailable region.
270 target_offset: u64,
271 /// Length in bytes of the unavailable region.
272 length: u32,
273 },
274
275 /// A [`crate::index::PatchSource::read`] call could not fill the requested
276 /// buffer.
277 #[error("patch source too short: offset {offset} requested {requested} bytes")]
278 PatchSourceTooShort {
279 /// Patch-file offset at which the read was attempted.
280 offset: u64,
281 /// Number of bytes the caller asked for.
282 requested: usize,
283 },
284
285 /// A [`crate::index::PatchSource::read`] call asked for a patch index
286 /// outside the source's configured chain.
287 #[error("patch index {patch} out of range: source carries {count} patches")]
288 PatchIndexOutOfRange {
289 /// Index that was requested.
290 patch: PatchIndex,
291 /// Total number of patches the source was constructed with.
292 count: usize,
293 },
294
295 /// An indexed-plan target or filesystem-op carried a path that escapes
296 /// the install root.
297 #[error("unsafe target path rejected by index builder: {0:?}")]
298 UnsafeTargetPath(String),
299
300 /// A persisted record's `schema_version` does not match this build.
301 #[error("{kind} schema version mismatch: persisted {found}, this build expects {expected}")]
302 SchemaVersionMismatch {
303 /// Type name whose schema the mismatch refers to.
304 kind: &'static str,
305 /// Persisted version.
306 found: SchemaVersion,
307 /// Version this build supports.
308 expected: SchemaVersion,
309 },
310
311 /// Two patches in the same chain shared a `name`.
312 #[error("duplicate patch in chain: {name:?}")]
313 DuplicatePatch {
314 /// The patch name that was added twice.
315 name: String,
316 },
317
318 /// A field value failed a builder invariant.
319 #[error("invalid field: {context}")]
320 InvalidField {
321 /// Human-readable description of which field was invalid and why.
322 context: &'static str,
323 },
324}
325
326impl From<std::io::Error> for IndexError {
327 fn from(source: std::io::Error) -> Self {
328 IndexError::Io { path: None, source }
329 }
330}
331
332impl From<ApplyError> for IndexError {
333 fn from(err: ApplyError) -> Self {
334 match err {
335 ApplyError::Parse(p) => IndexError::Parse(p),
336 ApplyError::Io { path, source } => IndexError::Io { path, source },
337 ApplyError::UnsupportedPlatform(id) => IndexError::UnsupportedPlatform(id),
338 ApplyError::Cancelled => IndexError::Cancelled,
339 ApplyError::SchemaVersionMismatch {
340 kind,
341 found,
342 expected,
343 } => IndexError::SchemaVersionMismatch {
344 kind,
345 found,
346 expected,
347 },
348 }
349 }
350}
351
352impl From<ApplyError> for VerifyError {
353 fn from(err: ApplyError) -> Self {
354 match err {
355 ApplyError::Io { path, source } => VerifyError::Io { path, source },
356 ApplyError::UnsupportedPlatform(id) => VerifyError::UnsupportedPlatform(id),
357 other => VerifyError::Io {
358 path: None,
359 source: std::io::Error::other(other.to_string()),
360 },
361 }
362 }
363}
364
365// ---------------------------------------------------------------------------
366// VerifyError — verifier failures
367// ---------------------------------------------------------------------------
368
369/// Failures produced by the verifier ([`crate::verify`] and
370/// [`crate::index::PlanVerifier`]).
371///
372/// `#[non_exhaustive]`: per-failure-mode splits (hash-algorithm mismatch,
373/// truncated source) may be added without a `SemVer` break.
374#[non_exhaustive]
375#[derive(Debug, thiserror::Error)]
376pub enum VerifyError {
377 /// An I/O failure while reading a target file.
378 #[error("I/O error{}: {source}", path.as_ref().map(|p| format!(" at {}", p.display())).unwrap_or_default())]
379 Io {
380 /// Path the failing operation was targeting, if known.
381 path: Option<PathBuf>,
382 /// Underlying [`std::io::Error`].
383 #[source]
384 source: std::io::Error,
385 },
386
387 /// `SqPack` path resolution refused to fall back to a default platform layout.
388 #[error("unsupported platform id {0}: cannot resolve SqPack path")]
389 UnsupportedPlatform(u16),
390
391 /// A plan or verification config violated a structural invariant.
392 #[error("invalid field: {context}")]
393 InvalidField {
394 /// Human-readable description of which field was invalid and why.
395 context: &'static str,
396 },
397
398 /// A region's observed CRC32 did not match the expected value.
399 #[error(
400 "CRC32 mismatch at target_offset {target_offset}: expected {expected:#010x}, got {actual:#010x}"
401 )]
402 Crc32Mismatch {
403 /// Target-file offset of the mismatched region.
404 target_offset: u64,
405 /// CRC32 declared by the plan's expectation.
406 expected: u32,
407 /// CRC32 computed over the bytes observed on disk.
408 actual: u32,
409 },
410}
411
412impl From<std::io::Error> for VerifyError {
413 fn from(source: std::io::Error) -> Self {
414 VerifyError::Io { path: None, source }
415 }
416}
417
418// ---------------------------------------------------------------------------
419// Error — top-level umbrella
420// ---------------------------------------------------------------------------
421
422/// `Result` alias for parsing-layer entry points.
423pub type ParseResult<T> = std::result::Result<T, ParseError>;
424/// `Result` alias for apply-layer entry points.
425pub type ApplyResult<T> = std::result::Result<T, ApplyError>;
426/// `Result` alias for indexed-pipeline entry points.
427pub type IndexResult<T> = std::result::Result<T, IndexError>;
428/// `Result` alias for verifier entry points.
429pub type VerifyResult<T> = std::result::Result<T, VerifyError>;
430
431/// Umbrella `Result` alias keyed on the top-level [`Error`] enum.
432///
433/// Use this at the edges of consumer code that just wants a single
434/// `?`-friendly error type covering every domain. Internal APIs continue to
435/// return their domain-specific aliases ([`ParseResult`], [`ApplyResult`],
436/// [`IndexResult`], [`VerifyResult`]).
437pub type Result<T> = std::result::Result<T, Error>;
438
439/// Top-level umbrella that every domain error converts into.
440///
441/// Convenient for callers that want a single `?`-friendly error type at the
442/// edges of their code. Domain-specific entry points still return their
443/// domain-specific type; callers who care about the distinction match on
444/// those, while callers who just want to bail out can use [`Error`].
445///
446/// `#[non_exhaustive]`: future top-level domains (e.g. a network-fetch
447/// layer if one ever lands here) would land as new variants.
448#[non_exhaustive]
449#[derive(Debug, thiserror::Error)]
450pub enum Error {
451 /// A parsing-layer failure.
452 #[error(transparent)]
453 Parse(#[from] ParseError),
454
455 /// An apply-layer failure.
456 #[error(transparent)]
457 Apply(#[from] ApplyError),
458
459 /// An indexed-pipeline failure.
460 #[error(transparent)]
461 Index(#[from] IndexError),
462
463 /// A verifier failure.
464 #[error(transparent)]
465 Verify(#[from] VerifyError),
466}