use std::io::Cursor;
use zipatch_rs::chunk::{ApplyFreeSpace, FileHeader, SqpkCommand};
use zipatch_rs::{Chunk, ZiPatchError, ZiPatchReader};
fn make_chunk(tag: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut crc_data = Vec::with_capacity(4 + body.len());
crc_data.extend_from_slice(tag);
crc_data.extend_from_slice(body);
let crc = crc32fast::hash(&crc_data);
let mut out = Vec::with_capacity(4 + 4 + body.len() + 4);
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
out.extend_from_slice(tag);
out.extend_from_slice(body);
out.extend_from_slice(&crc.to_be_bytes());
out
}
fn make_patch(chunks: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[
0x91, 0x5A, 0x49, 0x50, 0x41, 0x54, 0x43, 0x48, 0x0D, 0x0A, 0x1A, 0x0A,
]);
for chunk in chunks {
out.extend_from_slice(chunk);
}
out
}
#[test]
fn parses_fhdr_v2_then_eof() {
let mut body = Vec::new();
body.extend_from_slice(&(2u32 << 16).to_le_bytes());
body.extend_from_slice(b"D000");
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&[0u8; 8]);
let data = make_patch(&[make_chunk(b"FHDR", &body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
let Chunk::FileHeader(FileHeader::V2(h)) = reader.next().unwrap().unwrap() else {
panic!("expected FileHeader::V2");
};
assert_eq!(h.patch_type, *b"D000");
assert_eq!(h.entry_files, 1);
assert!(reader.next().is_none());
assert!(reader.is_complete());
}
#[test]
fn rejects_bad_magic() {
assert!(ZiPatchReader::new(Cursor::new(b"not a patch file at all")).is_err());
}
#[test]
fn checksum_verification_enabled_accepts_valid_chunk() {
let mut body = Vec::new();
body.extend_from_slice(&(2u32 << 16).to_le_bytes());
body.extend_from_slice(b"D000");
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&[0u8; 8]);
let data = make_patch(&[make_chunk(b"FHDR", &body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data))
.unwrap()
.verify_checksums();
assert!(matches!(
reader.next().unwrap().unwrap(),
Chunk::FileHeader(_)
));
}
#[test]
fn accepts_zeroed_crc_when_unverified() {
let mut body = Vec::new();
body.extend_from_slice(&(2u32 << 16).to_le_bytes());
body.extend_from_slice(b"D000");
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&[0u8; 8]);
let mut chunk = make_chunk(b"FHDR", &body);
let len = chunk.len();
chunk[len - 4..].fill(0);
let data = make_patch(&[chunk, make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data))
.unwrap()
.skip_checksum_verification();
assert!(matches!(
reader.next().unwrap().unwrap(),
Chunk::FileHeader(_)
));
}
#[test]
fn rejects_crc_mismatch() {
let mut body = Vec::new();
body.extend_from_slice(&(2u32 << 16).to_le_bytes());
body.extend_from_slice(b"D000");
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&[0u8; 8]);
let mut chunk = make_chunk(b"FHDR", &body);
let last = chunk.len() - 1;
chunk[last] ^= 0xFF;
let data = make_patch(&[chunk, make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
let err = reader.next().unwrap().unwrap_err();
let ZiPatchError::ChecksumMismatch { tag, .. } = err else {
panic!("expected ChecksumMismatch, got {err:?}");
};
assert_eq!(&tag, b"FHDR");
}
#[test]
fn rejects_unknown_tag() {
let data = make_patch(&[make_chunk(b"ZZZZ", &[]), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
assert!(matches!(
reader.next().unwrap(),
Err(ZiPatchError::UnknownChunkTag(_))
));
}
#[test]
fn iterator_stops_after_error() {
let mut body = Vec::new();
body.extend_from_slice(&(2u32 << 16).to_le_bytes());
body.extend_from_slice(b"D000");
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&[0u8; 8]);
let mut chunk = make_chunk(b"FHDR", &body);
let last = chunk.len() - 1;
chunk[last] ^= 0xFF;
let data = make_patch(&[chunk, make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
assert!(reader.next().unwrap().is_err());
assert!(reader.next().is_none());
}
#[test]
fn was_complete_true_after_eof() {
let data = make_patch(&[make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
assert!(reader.next().is_none());
assert!(reader.is_complete());
assert!(reader.next().is_none());
}
#[test]
fn parses_aply_chunk() {
let mut body = Vec::new();
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&1u32.to_be_bytes());
let data = make_patch(&[make_chunk(b"APLY", &body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
assert!(matches!(
reader.next().unwrap().unwrap(),
Chunk::ApplyOption(_)
));
}
#[test]
fn parses_apfs_chunk() {
let mut body = Vec::new();
body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes());
let data = make_patch(&[make_chunk(b"APFS", &body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
let chunk = reader.next().unwrap().unwrap();
assert!(matches!(
chunk,
Chunk::ApplyFreeSpace(ApplyFreeSpace {
unknown_a: 0,
unknown_b: 0
})
));
}
#[test]
fn parses_deld_chunk() {
let mut body = Vec::new();
body.extend_from_slice(&4u32.to_be_bytes()); body.extend_from_slice(b"test");
let data = make_patch(&[make_chunk(b"DELD", &body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
assert!(matches!(
reader.next().unwrap().unwrap(),
Chunk::DeleteDirectory(_)
));
}
#[test]
fn parses_sqpk_chunk() {
let mut cmd_body = Vec::new();
cmd_body.extend_from_slice(&[0u8; 3]); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&(-1i16).to_be_bytes()); cmd_body.extend_from_slice(&0i16.to_be_bytes()); cmd_body.extend_from_slice(&0u16.to_be_bytes()); cmd_body.extend_from_slice(&0u64.to_le_bytes()); cmd_body.extend_from_slice(&0u64.to_le_bytes());
let total_size = (5 + cmd_body.len()) as i32;
let mut sqpk_body = Vec::new();
sqpk_body.extend_from_slice(&total_size.to_be_bytes()); sqpk_body.push(b'T'); sqpk_body.extend_from_slice(&cmd_body);
let data = make_patch(&[make_chunk(b"SQPK", &sqpk_body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
assert!(matches!(
reader.next().unwrap().unwrap(),
Chunk::Sqpk(SqpkCommand::TargetInfo(_))
));
}
#[test]
fn parses_fhdr_v3() {
let mut body = Vec::new();
body.extend_from_slice(&(3u32 << 16).to_le_bytes());
body.extend_from_slice(b"D000");
body.extend_from_slice(&5u32.to_be_bytes());
body.extend_from_slice(&7u32.to_be_bytes());
body.extend_from_slice(&3u32.to_be_bytes());
body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&10u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&100u32.to_be_bytes());
body.extend_from_slice(&20u32.to_be_bytes());
body.extend_from_slice(&5u32.to_be_bytes());
body.extend_from_slice(&8u32.to_be_bytes());
body.extend_from_slice(&12u32.to_be_bytes());
body.extend_from_slice(&30u32.to_be_bytes());
body.extend_from_slice(&[0u8; 0xB8]);
let data = make_patch(&[make_chunk(b"FHDR", &body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
let Chunk::FileHeader(FileHeader::V3(h)) = reader.next().unwrap().unwrap() else {
panic!("expected FileHeader::V3");
};
assert_eq!(h.patch_type, *b"D000");
assert_eq!(h.entry_files, 5);
assert_eq!(h.add_directories, 7);
assert_eq!(h.delete_directories, 3);
assert_eq!(h.delete_data_size, 2 | (1u64 << 32));
assert_eq!(h.commands, 100);
assert_eq!(h.sqpk_add_commands, 20);
assert_eq!(h.sqpk_file_commands, 30);
}