zipatch_rs/error.rs
1/// All failures returned by parsing or applying a `ZiPatch` stream.
2///
3/// Both the parsing layer ([`crate::chunk`]) and the apply layer
4/// ([`crate::apply`]) surface errors through this single enum, so callers
5/// need only one error type in their `match` arms.
6///
7/// # Mapping from standard errors
8///
9/// - [`std::io::Error`] converts automatically via `#[from]` into [`ZiPatchError::Io`].
10/// - [`std::string::FromUtf8Error`] converts automatically into [`ZiPatchError::Utf8Error`].
11/// - [`binrw::Error`] converts automatically into [`ZiPatchError::BinrwError`].
12///
13/// # Example
14///
15/// ```rust
16/// use zipatch_rs::ZiPatchError;
17///
18/// fn describe(e: &ZiPatchError) -> &'static str {
19/// match e {
20/// ZiPatchError::Io(_) => "I/O error",
21/// ZiPatchError::InvalidMagic => "not a ZiPatch file",
22/// ZiPatchError::UnknownChunkTag(_) => "unrecognised chunk",
23/// ZiPatchError::ChecksumMismatch {..}=> "corrupt chunk",
24/// ZiPatchError::TruncatedPatch => "download incomplete",
25/// _ => "other error",
26/// }
27/// }
28/// ```
29#[non_exhaustive]
30#[derive(Debug, thiserror::Error)]
31pub enum ZiPatchError {
32 /// Underlying I/O failure from the patch source or target filesystem.
33 ///
34 /// Raised by the parsing layer when reading from the patch stream fails
35 /// (e.g. a network read error), and by the apply layer when any filesystem
36 /// operation fails (open, seek, write, delete, create-dir).
37 ///
38 /// The wrapped [`std::io::Error`] carries the OS-level error code and kind.
39 /// Use [`std::io::Error::kind`] to distinguish `NotFound`, `PermissionDenied`,
40 /// and similar conditions.
41 #[error("I/O error: {0}")]
42 Io(#[from] std::io::Error),
43
44 /// The 12-byte `ZiPatch` magic header was missing or did not match.
45 ///
46 /// Raised by [`ZiPatchReader::new`](crate::ZiPatchReader::new) and
47 /// [`ZiPatchReader::from_path`](crate::ZiPatchReader::from_path) when
48 /// the first 12 bytes of the source do not equal the expected magic
49 /// sequence `\x91ZIPATCH\r\n\x1a\n`. Typically indicates the caller opened
50 /// the wrong file, or the file is corrupted at the very start.
51 #[error("invalid magic bytes")]
52 InvalidMagic,
53
54 /// A 4-byte chunk tag was not recognised by the parser.
55 ///
56 /// Raised by the chunk dispatcher in [`crate::chunk`] when a tag does not
57 /// match any of `FHDR`, `APLY`, `APFS`, `ADIR`, `DELD`, `SQPK`, or
58 /// `EOF_`. The raw tag bytes are preserved so callers can log them for
59 /// diagnostic purposes.
60 ///
61 /// This can occur with genuinely new chunk types introduced by Square Enix
62 /// in future patches. Upgrading the library or filing an issue is the
63 /// appropriate response.
64 #[error("unknown chunk tag: {0:?}")]
65 UnknownChunkTag([u8; 4]),
66
67 /// A SQPK sub-command byte was not recognised by the parser.
68 ///
69 /// Raised by the SQPK dispatcher in [`crate::chunk`] when the single-byte
70 /// command code inside an `SQPK` chunk does not match any of the known
71 /// values (`A`, `D`, `E`, `F`, `H`, `I`, `T`, `X`). The raw byte is
72 /// preserved for diagnostics.
73 #[error("unknown SQPK command: {0:#x}")]
74 UnknownSqpkCommand(u8),
75
76 /// A chunk's recorded CRC32 did not match the computed CRC32.
77 ///
78 /// Raised by the chunk framing code in [`crate::chunk`] when CRC32
79 /// verification is enabled (the default) and the stored checksum in the
80 /// patch stream does not match the checksum computed over `tag ++ body`.
81 ///
82 /// This indicates a corrupt or partially-written patch file. Disable
83 /// verification with
84 /// [`ZiPatchReader::skip_checksum_verification`](crate::ZiPatchReader::skip_checksum_verification)
85 /// if the source has already been verified out-of-band.
86 ///
87 /// The `tag`, `expected`, and `actual` fields provide enough information
88 /// to log a precise diagnostic message.
89 #[error("CRC32 mismatch on chunk {tag:?}: expected {expected:#010x}, got {actual:#010x}")]
90 ChecksumMismatch {
91 /// Tag of the chunk whose checksum failed.
92 tag: [u8; 4],
93 /// CRC32 stored in the patch file.
94 expected: u32,
95 /// CRC32 computed over the actual chunk bytes.
96 actual: u32,
97 },
98
99 /// DEFLATE decompression of a `SqpkFile` block failed.
100 ///
101 /// Raised by the `SqpkFile` apply logic when `flate2` cannot decompress
102 /// a compressed block payload inside an `AddFile` operation. The wrapped
103 /// [`std::io::Error`] is the decompressor's error, not a filesystem error;
104 /// it is stored as a `#[source]` rather than `#[from]` to keep it distinct
105 /// from [`ZiPatchError::Io`].
106 #[error("decompression error")]
107 Decompress(#[source] std::io::Error),
108
109 /// A field value failed a parser invariant (e.g. negative size).
110 ///
111 /// Raised by chunk-specific parsers in [`crate::chunk`] when a field
112 /// value is syntactically valid (i.e. could be parsed) but violates a
113 /// semantic constraint required by the format — for example, a length
114 /// field that is negative, or a size that overflows the expected range.
115 ///
116 /// The `context` string names the specific field that failed its check,
117 /// giving enough information to locate the offending position in the
118 /// binary format documentation.
119 #[error("invalid field: {context}")]
120 InvalidField {
121 /// Human-readable description of which field was invalid and why.
122 context: &'static str,
123 },
124
125 /// A chunk declared a size larger than the parser's maximum (512 MiB).
126 ///
127 /// Raised by the chunk framing code in [`crate::chunk`] when the
128 /// `body_len` field of a chunk frame exceeds 512 MiB. This guard prevents
129 /// allocating an arbitrarily large buffer from a malformed or malicious
130 /// patch stream. The preserved size value can be logged for diagnostics.
131 #[error("chunk size {0} exceeds maximum")]
132 OversizedChunk(usize),
133
134 /// A `SqpkFile` operation byte was not recognised.
135 ///
136 /// Raised by the `SqpkFile` parser when the single-byte `operation` field
137 /// does not match any of `A` (`AddFile`), `R` (`RemoveAll`), `D` (`DeleteFile`),
138 /// or `M` (`MakeDirTree`). The raw byte is preserved for diagnostics.
139 #[error("unknown SqpkFile operation: {0:#02x}")]
140 UnknownFileOperation(u8),
141
142 /// A UTF-8 decode failed when reading a path or name field.
143 ///
144 /// Raised by the parsing layer when a length-prefixed byte string (e.g. a
145 /// directory name in an `ADIR` chunk or a file path in a `SqpkFile` chunk)
146 /// is not valid UTF-8. FFXIV patch paths are documented as ASCII, so this
147 /// error indicates either a corrupt patch file or an undocumented encoding
148 /// extension.
149 #[error("UTF-8 decode error")]
150 Utf8Error(#[from] std::string::FromUtf8Error),
151
152 /// A `binrw` parser produced an error; wraps the underlying cause.
153 ///
154 /// Several chunk types (notably those using `#[derive(BinRead)]`) delegate
155 /// their parsing to `binrw`. When `binrw` encounters an unexpected byte
156 /// pattern or short read, it returns a [`binrw::Error`] which is wrapped
157 /// here via `#[from]`.
158 ///
159 /// The inner error message usually identifies the field name and byte
160 /// offset where parsing failed.
161 #[error("binrw parse error: {0}")]
162 BinrwError(#[from] binrw::Error),
163
164 /// A `SqpkFile` carried a negative `file_offset` that cannot be applied.
165 ///
166 /// Raised by the `AddFile` arm of the `SqpkFile` apply logic when
167 /// `cmd.file_offset` is negative and therefore cannot be converted to a
168 /// [`u64`] seek position. The wire format stores this field as `i64`;
169 /// a non-negative value is the invariant required for correct application.
170 ///
171 /// The preserved `i64` value can be logged to report the exact field content.
172 #[error("negative file_offset in SqpkFile: {0}")]
173 NegativeFileOffset(i64),
174
175 /// Stream ended without an `EOF_` chunk; download or copy was truncated.
176 ///
177 /// Raised by the chunk framing code when attempting to read the 4-byte
178 /// `body_len` field of the next chunk returns an unexpected EOF. This
179 /// indicates the patch stream ended before the mandatory `EOF_` terminator
180 /// chunk was encountered — the patch file was likely incompletely
181 /// downloaded or written to disk.
182 ///
183 /// Use [`ZiPatchReader::is_complete`](crate::ZiPatchReader::is_complete)
184 /// after iteration to distinguish this case from a clean end of stream.
185 #[error("patch file ended without EOF_ chunk (truncated download?)")]
186 TruncatedPatch,
187}