1use crate::reader::ReadExt;
2use crate::{Result, ZiPatchError};
3use flate2::read::DeflateDecoder;
4use std::borrow::Cow;
5use std::io::{Cursor, Read, Write};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SqpkFileOperation {
10 AddFile,
12 RemoveAll,
14 DeleteFile,
16 MakeDirTree,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct SqpkCompressedBlock {
23 is_compressed: bool,
24 decompressed_size: usize,
25 data: Vec<u8>, }
27
28impl SqpkCompressedBlock {
29 #[must_use]
31 pub fn new(is_compressed: bool, decompressed_size: usize, data: Vec<u8>) -> Self {
32 Self {
33 is_compressed,
34 decompressed_size,
35 data,
36 }
37 }
38
39 fn read<R: Read>(r: &mut R) -> Result<Self> {
40 let header_size_raw = r.read_i32_le()?;
41 r.skip(4)?; let compressed_size = r.read_i32_le()?;
43 let decompressed_size_raw = r.read_i32_le()?;
44
45 if header_size_raw < 0 {
46 return Err(ZiPatchError::InvalidField {
47 context: "negative header_size in block",
48 });
49 }
50 if decompressed_size_raw < 0 {
51 return Err(ZiPatchError::InvalidField {
52 context: "negative decompressed_size in block",
53 });
54 }
55 let is_compressed = compressed_size != 0x7d00;
56 if is_compressed && compressed_size < 0 {
57 return Err(ZiPatchError::InvalidField {
58 context: "negative compressed_size in block",
59 });
60 }
61
62 let header_size = header_size_raw as usize;
63 let decompressed_size = decompressed_size_raw as usize;
64 let data_len = if is_compressed {
65 compressed_size
66 } else {
67 decompressed_size_raw
68 };
69 let block_len = ((data_len as u32 + 143) & !127u32) as usize;
70 let data = if is_compressed {
71 r.read_exact_vec(block_len - header_size)?
72 } else {
73 let d = r.read_exact_vec(decompressed_size)?;
74 r.skip((block_len - header_size - decompressed_size) as u64)?;
75 d
76 };
77 Ok(SqpkCompressedBlock {
78 is_compressed,
79 decompressed_size,
80 data,
81 })
82 }
83
84 pub fn decompress_into(&self, w: &mut impl Write) -> Result<()> {
90 if self.is_compressed {
91 std::io::copy(&mut DeflateDecoder::new(self.data.as_slice()), w)
92 .map_err(ZiPatchError::Decompress)?;
93 } else {
94 w.write_all(&self.data)?;
95 }
96 Ok(())
97 }
98
99 pub fn decompress(&self) -> crate::Result<Cow<'_, [u8]>> {
104 if self.is_compressed {
105 let mut out = Vec::with_capacity(self.decompressed_size);
106 self.decompress_into(&mut out)?;
107 Ok(Cow::Owned(out))
108 } else {
109 Ok(Cow::Borrowed(&self.data))
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct SqpkFile {
117 pub operation: SqpkFileOperation,
119 pub file_offset: i64,
121 pub file_size: i64,
123 pub expansion_id: u16,
125 pub path: String,
127 pub block_source_offsets: Vec<u64>,
132 pub blocks: Vec<SqpkCompressedBlock>,
134}
135
136pub(crate) fn parse(body: &[u8]) -> Result<SqpkFile> {
137 let mut c = Cursor::new(body);
138
139 let operation = match c.read_u8()? {
140 b'A' => SqpkFileOperation::AddFile,
141 b'R' => SqpkFileOperation::RemoveAll,
142 b'D' => SqpkFileOperation::DeleteFile,
143 b'M' => SqpkFileOperation::MakeDirTree,
144 b => {
145 return Err(ZiPatchError::UnknownFileOperation(b));
146 }
147 };
148 c.skip(2)?; let file_offset = c.read_u64_be()? as i64;
151 let file_size = c.read_u64_be()? as i64;
152 let path_len = c.read_u32_be()?;
153 let expansion_id = c.read_u16_be()?;
154 c.skip(2)?; let path_bytes = c.read_exact_vec(path_len as usize)?;
157 let path = String::from_utf8(path_bytes)
158 .map(|s| s.trim_end_matches('\0').to_owned())
159 .map_err(ZiPatchError::Utf8Error)?;
160
161 let (blocks, block_source_offsets) = if matches!(operation, SqpkFileOperation::AddFile) {
162 let mut blocks = Vec::new();
163 let mut offsets = Vec::new();
164 while (c.position() as usize) < body.len() {
165 offsets.push(c.position() + 16);
167 blocks.push(SqpkCompressedBlock::read(&mut c)?);
168 }
169 (blocks, offsets)
170 } else {
171 (Vec::new(), Vec::new())
172 };
173
174 Ok(SqpkFile {
175 operation,
176 file_offset,
177 file_size,
178 expansion_id,
179 path,
180 block_source_offsets,
181 blocks,
182 })
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn make_header(
190 op: u8,
191 file_offset: i64,
192 file_size: i64,
193 path: &[u8],
194 expansion_id: u16,
195 ) -> Vec<u8> {
196 let mut body = Vec::new();
197 body.push(op);
198 body.extend_from_slice(&[0u8; 2]); body.extend_from_slice(&(file_offset as u64).to_be_bytes());
200 body.extend_from_slice(&(file_size as u64).to_be_bytes());
201 body.extend_from_slice(&(path.len() as u32).to_be_bytes());
202 body.extend_from_slice(&expansion_id.to_be_bytes());
203 body.extend_from_slice(&[0u8; 2]); body.extend_from_slice(path);
205 body
206 }
207
208 #[test]
209 fn parses_add_file_no_blocks() {
210 let body = make_header(b'A', 0, 512, b"test\0", 1);
211 let cmd = parse(&body).unwrap();
212 assert!(matches!(cmd.operation, SqpkFileOperation::AddFile));
213 assert_eq!(cmd.file_offset, 0);
214 assert_eq!(cmd.file_size, 512);
215 assert_eq!(cmd.expansion_id, 1);
216 assert_eq!(cmd.path, "test");
217 assert!(cmd.blocks.is_empty());
218 assert!(cmd.block_source_offsets.is_empty());
219 }
220
221 #[test]
222 fn parses_add_file_uncompressed_block() {
223 let mut body = make_header(b'A', 0, 0, b"\0", 0);
225 body.extend_from_slice(&16i32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0x7d00i32.to_le_bytes()); body.extend_from_slice(&8i32.to_le_bytes()); body.extend_from_slice(&[0xABu8; 8]); body.extend_from_slice(&[0u8; 104]); let cmd = parse(&body).unwrap();
234 assert_eq!(cmd.blocks.len(), 1);
235 let block = &cmd.blocks[0];
236 assert!(!block.is_compressed);
237 assert_eq!(block.decompressed_size, 8);
238 assert_eq!(block.data.len(), 8);
239 assert!(block.data.iter().all(|&b| b == 0xAB));
240 assert_eq!(block.decompress().unwrap(), vec![0xABu8; 8]);
241 assert_eq!(cmd.block_source_offsets, vec![44u64]); }
243
244 #[test]
245 fn parses_remove_all_operation() {
246 let body = make_header(b'R', 0, 0, b"\0", 0);
247 let cmd = parse(&body).unwrap();
248 assert!(matches!(cmd.operation, SqpkFileOperation::RemoveAll));
249 assert!(cmd.blocks.is_empty());
250 assert!(cmd.block_source_offsets.is_empty());
251 }
252
253 #[test]
254 fn parses_delete_file_operation() {
255 let body = make_header(b'D', 0, 0, b"sqpack/foo.dat\0", 0);
256 let cmd = parse(&body).unwrap();
257 assert!(matches!(cmd.operation, SqpkFileOperation::DeleteFile));
258 assert_eq!(cmd.path, "sqpack/foo.dat");
259 }
260
261 #[test]
262 fn parses_make_dir_tree_operation() {
263 let body = make_header(b'M', 0, 0, b"sqpack/ex1\0", 0);
264 let cmd = parse(&body).unwrap();
265 assert!(matches!(cmd.operation, SqpkFileOperation::MakeDirTree));
266 assert_eq!(cmd.path, "sqpack/ex1");
267 }
268
269 #[test]
270 fn rejects_unknown_operation() {
271 let body = make_header(b'Z', 0, 0, b"\0", 0);
272 assert!(parse(&body).is_err());
273 }
274
275 fn block_with_sizes(header_size: i32, compressed_size: i32, decompressed_size: i32) -> Vec<u8> {
276 let mut body = make_header(b'A', 0, 0, b"\0", 0);
277 body.extend_from_slice(&header_size.to_le_bytes());
278 body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&compressed_size.to_le_bytes());
280 body.extend_from_slice(&decompressed_size.to_le_bytes());
281 body
282 }
283
284 #[test]
285 fn rejects_negative_header_size() {
286 let body = block_with_sizes(-1, 0x7d00, 0);
287 let Err(ZiPatchError::InvalidField { context }) = parse(&body) else {
288 panic!("expected InvalidField for negative header_size");
289 };
290 assert!(
291 context.contains("header_size"),
292 "unexpected context: {context}"
293 );
294 }
295
296 #[test]
297 fn rejects_negative_decompressed_size() {
298 let body = block_with_sizes(16, 0x7d00, -1);
299 let Err(ZiPatchError::InvalidField { context }) = parse(&body) else {
300 panic!("expected InvalidField for negative decompressed_size");
301 };
302 assert!(
303 context.contains("decompressed_size"),
304 "unexpected context: {context}"
305 );
306 }
307
308 #[test]
309 fn rejects_negative_compressed_size() {
310 let body = block_with_sizes(16, -1, 8);
312 let Err(ZiPatchError::InvalidField { context }) = parse(&body) else {
313 panic!("expected InvalidField for negative compressed_size");
314 };
315 assert!(
316 context.contains("compressed_size"),
317 "unexpected context: {context}"
318 );
319 }
320
321 #[test]
322 fn rejects_invalid_utf8_in_path() {
323 let body = make_header(b'D', 0, 0, &[0xFFu8], 0);
325 assert!(matches!(parse(&body), Err(ZiPatchError::Utf8Error(_))));
326 }
327
328 #[test]
329 fn decompress_into_uncompressed_writes_data_verbatim() {
330 let block = SqpkCompressedBlock::new(false, 5, b"hello".to_vec());
332 let mut out = Vec::new();
333 block.decompress_into(&mut out).unwrap();
334 assert_eq!(out, b"hello");
335 }
336
337 #[test]
338 fn decompress_returns_borrowed_for_uncompressed() {
339 let block = SqpkCompressedBlock::new(false, 4, b"data".to_vec());
341 let cow = block.decompress().unwrap();
342 assert!(matches!(cow, Cow::Borrowed(_)));
343 assert_eq!(&*cow, b"data");
344 }
345
346 #[test]
347 fn decompress_into_compressed_propagates_decompress_error() {
348 let block = SqpkCompressedBlock::new(true, 16, vec![0xFFu8; 16]);
350 let mut out = Vec::new();
351 assert!(matches!(
352 block.decompress_into(&mut out),
353 Err(ZiPatchError::Decompress(_))
354 ));
355 assert!(matches!(
357 block.decompress(),
358 Err(ZiPatchError::Decompress(_))
359 ));
360 }
361
362 #[test]
363 fn parses_compressed_block() {
364 use flate2::Compression;
365 use flate2::write::DeflateEncoder;
366 use std::io::Write;
367
368 let raw: &[u8] = b"hello compressed world";
369 let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
370 enc.write_all(raw).unwrap();
371 let compressed = enc.finish().unwrap();
372
373 let header_size: i32 = 16;
374 let compressed_size = compressed.len() as i32;
375 let decompressed_size = raw.len() as i32;
376 let block_len = ((compressed_size as u32 + 143) & !127) as usize;
377 let trailing_pad = block_len - header_size as usize - compressed.len();
378
379 let mut body = make_header(b'A', 0, 0, b"\0", 0);
381 body.extend_from_slice(&header_size.to_le_bytes());
382 body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&compressed_size.to_le_bytes());
384 body.extend_from_slice(&decompressed_size.to_le_bytes());
385 body.extend_from_slice(&compressed);
386 body.extend_from_slice(&vec![0u8; trailing_pad]);
387
388 let cmd = parse(&body).unwrap();
389 assert_eq!(cmd.blocks.len(), 1);
390 let block = &cmd.blocks[0];
391 assert!(block.is_compressed);
392 assert_eq!(block.decompressed_size, raw.len());
393 assert_eq!(block.decompress().unwrap(), raw);
394 assert_eq!(cmd.block_source_offsets, vec![44u64]); }
396}