1pub(crate) mod adir;
2pub(crate) mod afsp;
3pub(crate) mod aply;
4pub(crate) mod ddir;
5pub(crate) mod fhdr;
6pub(crate) mod sqpk;
7pub(crate) mod util;
8
9pub use adir::AddDirectory;
10pub use afsp::ApplyFreeSpace;
11pub use aply::{ApplyOption, ApplyOptionKind};
12pub use ddir::DeleteDirectory;
13pub use fhdr::{FileHeader, FileHeaderV2, FileHeaderV3};
14pub use sqpk::{SqpackFile, SqpkCommand};
15pub use sqpk::{
17 IndexCommand, SqpkAddData, SqpkCompressedBlock, SqpkDeleteData, SqpkExpandData, SqpkFile,
18 SqpkFileOperation, SqpkHeader, SqpkHeaderTarget, SqpkIndex, SqpkPatchInfo, SqpkTargetInfo,
19 TargetFileKind, TargetHeaderKind,
20};
21
22use crate::reader::ReadExt;
23use crate::{Result, ZiPatchError};
24use tracing::trace;
25
26const MAGIC: [u8; 12] = [
27 0x91, 0x5A, 0x49, 0x50, 0x41, 0x54, 0x43, 0x48, 0x0D, 0x0A, 0x1A, 0x0A,
28];
29
30const MAX_CHUNK_SIZE: usize = 512 * 1024 * 1024;
31
32#[non_exhaustive]
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum Chunk {
39 FileHeader(FileHeader),
41 ApplyOption(ApplyOption),
43 ApplyFreeSpace(ApplyFreeSpace),
45 AddDirectory(AddDirectory),
47 DeleteDirectory(DeleteDirectory),
49 Sqpk(SqpkCommand),
51 EndOfFile,
53}
54
55pub(crate) fn parse_chunk<R: std::io::Read>(r: &mut R, verify_checksums: bool) -> Result<Chunk> {
56 let size = match r.read_u32_be() {
57 Ok(s) => s as usize,
58 Err(ZiPatchError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
59 return Err(ZiPatchError::TruncatedPatch);
60 }
61 Err(e) => return Err(e),
62 };
63 if size > MAX_CHUNK_SIZE {
64 return Err(ZiPatchError::OversizedChunk(size));
65 }
66 let buf = r.read_exact_vec(size + 8)?;
68
69 let tag: [u8; 4] = buf[..4].try_into().unwrap();
70
71 let actual_crc = crc32fast::hash(&buf[..size + 4]);
72 let expected_crc = u32::from_be_bytes(buf[size + 4..].try_into().unwrap());
73 if verify_checksums && actual_crc != expected_crc {
74 return Err(ZiPatchError::ChecksumMismatch {
75 tag,
76 expected: expected_crc,
77 actual: actual_crc,
78 });
79 }
80
81 let body = &buf[4..size + 4];
82
83 trace!(tag = %String::from_utf8_lossy(&tag), "chunk");
84
85 match &tag {
86 b"EOF_" => Ok(Chunk::EndOfFile),
87 b"FHDR" => Ok(Chunk::FileHeader(fhdr::parse(body)?)),
88 b"APLY" => Ok(Chunk::ApplyOption(aply::parse(body)?)),
89 b"APFS" => Ok(Chunk::ApplyFreeSpace(afsp::parse(body)?)),
90 b"ADIR" => Ok(Chunk::AddDirectory(adir::parse(body)?)),
91 b"DELD" => Ok(Chunk::DeleteDirectory(ddir::parse(body)?)),
92 b"SQPK" => Ok(Chunk::Sqpk(sqpk::parse_sqpk(body)?)),
93 _ => Err(ZiPatchError::UnknownChunkTag(tag)),
94 }
95}
96
97#[derive(Debug)]
104pub struct ZiPatchReader<R> {
105 inner: R,
106 done: bool,
107 verify_checksums: bool,
108 eof_seen: bool,
109}
110
111impl<R: std::io::Read> ZiPatchReader<R> {
112 pub fn new(mut reader: R) -> Result<Self> {
116 let magic = reader.read_exact_vec(12)?;
117 if magic.as_slice() != MAGIC {
118 return Err(ZiPatchError::InvalidMagic);
119 }
120 Ok(Self {
121 inner: reader,
122 done: false,
123 verify_checksums: true,
124 eof_seen: false,
125 })
126 }
127
128 #[must_use]
130 pub fn verify_checksums(mut self) -> Self {
131 self.verify_checksums = true;
132 self
133 }
134
135 #[must_use]
139 pub fn skip_checksum_verification(mut self) -> Self {
140 self.verify_checksums = false;
141 self
142 }
143
144 pub fn is_complete(&self) -> bool {
146 self.eof_seen
147 }
148}
149
150impl ZiPatchReader<std::io::BufReader<std::fs::File>> {
151 pub fn from_path(path: impl AsRef<std::path::Path>) -> crate::Result<Self> {
153 let file = std::fs::File::open(path)?;
154 Self::new(std::io::BufReader::new(file))
155 }
156}
157
158impl<R: std::io::Read> Iterator for ZiPatchReader<R> {
159 type Item = Result<Chunk>;
160
161 fn next(&mut self) -> Option<Self::Item> {
162 if self.done {
163 return None;
164 }
165 match parse_chunk(&mut self.inner, self.verify_checksums) {
166 Ok(Chunk::EndOfFile) => {
167 self.done = true;
168 self.eof_seen = true;
169 None
170 }
171 Ok(chunk) => Some(Ok(chunk)),
172 Err(e) => {
173 self.done = true;
174 Some(Err(e))
175 }
176 }
177 }
178}
179
180impl<R: std::io::Read> std::iter::FusedIterator for ZiPatchReader<R> {}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::io::Cursor;
186
187 fn build_chunk(tag: &[u8; 4], body: &[u8]) -> Vec<u8> {
189 let size = body.len() as u32;
190 let mut buf = Vec::with_capacity(8 + body.len() + 4);
191 buf.extend_from_slice(&size.to_be_bytes());
192 buf.extend_from_slice(tag);
193 buf.extend_from_slice(body);
194 let crc = {
195 let mut crc_input = Vec::with_capacity(4 + body.len());
196 crc_input.extend_from_slice(tag);
197 crc_input.extend_from_slice(body);
198 crc32fast::hash(&crc_input)
199 };
200 buf.extend_from_slice(&crc.to_be_bytes());
201 buf
202 }
203
204 #[test]
205 fn truncated_at_chunk_boundary_maps_to_truncated_patch() {
206 let mut patch = Vec::new();
214 patch.extend_from_slice(&MAGIC);
215 let mut reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
217 let first = reader.next().expect("iterator must yield once");
218 match first {
219 Err(ZiPatchError::TruncatedPatch) => {}
220 other => panic!("expected TruncatedPatch, got {other:?}"),
221 }
222 assert!(!reader.is_complete());
223 }
224
225 #[test]
226 fn truncated_after_one_chunk_maps_to_truncated_patch() {
227 let mut adir_body = Vec::new();
231 adir_body.extend_from_slice(&4u32.to_be_bytes()); adir_body.extend_from_slice(b"test"); let chunk = build_chunk(b"ADIR", &adir_body);
234
235 let mut patch = Vec::new();
236 patch.extend_from_slice(&MAGIC);
237 patch.extend_from_slice(&chunk);
238
239 let mut reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
240 let first = reader.next().expect("first chunk");
241 assert!(first.is_ok(), "first chunk should parse: {first:?}");
242 let second = reader.next().expect("iterator yields TruncatedPatch");
243 match second {
244 Err(ZiPatchError::TruncatedPatch) => {}
245 other => panic!("expected TruncatedPatch, got {other:?}"),
246 }
247 assert!(!reader.is_complete());
248 }
249
250 #[test]
251 fn oversized_chunk_size_rejected() {
252 let bytes = [0xFFu8, 0xFF, 0xFF, 0xFF];
254 let mut cur = Cursor::new(&bytes[..]);
255 let Err(ZiPatchError::OversizedChunk(size)) = parse_chunk(&mut cur, false) else {
256 panic!("expected OversizedChunk for oversized chunk")
257 };
258 assert!(
259 size > MAX_CHUNK_SIZE,
260 "expected size > MAX_CHUNK_SIZE, got {size}"
261 );
262 }
263
264 #[test]
265 fn from_path_opens_and_parses_patch_file() {
266 let mut bytes = Vec::new();
268 bytes.extend_from_slice(&MAGIC);
269 bytes.extend_from_slice(&build_chunk(b"EOF_", &[]));
270
271 let tmp = tempfile::tempdir().unwrap();
272 let file_path = tmp.path().join("test.patch");
273 std::fs::write(&file_path, &bytes).unwrap();
274
275 let mut reader = ZiPatchReader::from_path(&file_path).expect("from_path opens patch file");
276 assert!(reader.next().is_none(), "EOF_ should terminate iteration");
277 assert!(reader.is_complete());
278 }
279
280 #[test]
281 fn from_path_returns_io_error_on_missing_file() {
282 let tmp = tempfile::tempdir().unwrap();
283 let file_path = tmp.path().join("nonexistent.patch");
284 assert!(matches!(
285 ZiPatchReader::from_path(&file_path),
286 Err(ZiPatchError::Io(_))
287 ));
288 }
289}