use std::io::Cursor;
use zipatch_rs::chunk::{ApplyFreeSpace, FileHeader, SqpkAddData, SqpkCommand};
use zipatch_rs::test_utils::{make_chunk, make_patch};
use zipatch_rs::{Chunk, ZiPatchError, ZiPatchReader};
#[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);
}
fn make_sqpk_add_data_body(
main_id: u16,
sub_id: u16,
file_id: u32,
block_offset_raw: u32,
data_bytes_raw: u32,
block_delete_number_raw: u32,
) -> Vec<u8> {
let data_len = (data_bytes_raw as usize) * 128;
let total = 5 + 23 + data_len;
let mut body = Vec::with_capacity(total);
body.extend_from_slice(&(total as i32).to_be_bytes()); body.push(b'A'); body.extend_from_slice(&[0u8; 3]); body.extend_from_slice(&main_id.to_be_bytes());
body.extend_from_slice(&sub_id.to_be_bytes());
body.extend_from_slice(&file_id.to_be_bytes());
body.extend_from_slice(&block_offset_raw.to_be_bytes());
body.extend_from_slice(&data_bytes_raw.to_be_bytes());
body.extend_from_slice(&block_delete_number_raw.to_be_bytes());
body.extend(std::iter::repeat(0u8).take(data_len)); body
}
#[test]
fn parses_sqpk_add_data_chunk_via_zipatch_reader() {
let sqpk_body = make_sqpk_add_data_body(
1, 2, 3, 4, 1, 0, );
let data = make_patch(&[make_chunk(b"SQPK", &sqpk_body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
let Chunk::Sqpk(SqpkCommand::AddData(cmd)) = reader.next().unwrap().unwrap() else {
panic!("expected Chunk::Sqpk(SqpkCommand::AddData(_))");
};
let SqpkAddData {
target_file,
block_offset,
data_bytes,
block_delete_number,
data,
} = *cmd;
assert_eq!(target_file.main_id, 1);
assert_eq!(target_file.sub_id, 2);
assert_eq!(target_file.file_id, 3);
assert_eq!(block_offset, 512); assert_eq!(data_bytes, 128); assert_eq!(block_delete_number, 0);
assert_eq!(data.len(), 128);
assert!(data.iter().all(|&b| b == 0));
assert!(reader.next().is_none());
assert!(reader.is_complete());
}
#[test]
fn sqpk_add_data_fast_path_inner_size_mismatch_returns_invalid_field() {
let mut sqpk_body = make_sqpk_add_data_body(0, 0, 0, 0, 0, 0);
let wrong_size = (sqpk_body.len() as i32) + 99;
sqpk_body[0..4].copy_from_slice(&wrong_size.to_be_bytes());
let data = make_patch(&[make_chunk(b"SQPK", &sqpk_body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
match reader.next().unwrap() {
Err(ZiPatchError::InvalidField { context }) => {
assert!(
context.contains("inner size"),
"error context must mention 'inner size', got: {context}"
);
}
other => panic!("expected InvalidField for inner_size mismatch, got {other:?}"),
}
}
#[test]
fn sqpk_add_data_fast_path_data_bytes_mismatch_returns_invalid_field() {
let mut sqpk_body = make_sqpk_add_data_body(0, 0, 0, 0, 0, 0);
let data_bytes_raw_offset = 20;
let inflated: u32 = 999; sqpk_body[data_bytes_raw_offset..data_bytes_raw_offset + 4]
.copy_from_slice(&inflated.to_be_bytes());
let data = make_patch(&[make_chunk(b"SQPK", &sqpk_body), make_chunk(b"EOF_", &[])]);
let mut reader = ZiPatchReader::new(Cursor::new(data)).unwrap();
match reader.next().unwrap() {
Err(ZiPatchError::InvalidField { context }) => {
assert!(
context.contains("data_bytes"),
"error context must mention 'data_bytes', got: {context}"
);
}
other => panic!("expected InvalidField for data_bytes mismatch, got {other:?}"),
}
}
#[test]
fn sqpk_add_data_fast_path_crc_mismatch_returns_checksum_mismatch() {
let sqpk_body = make_sqpk_add_data_body(0, 0, 0, 0, 1, 0);
let mut chunk = make_chunk(b"SQPK", &sqpk_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();
match reader.next().unwrap() {
Err(ZiPatchError::ChecksumMismatch { tag, .. }) => {
assert_eq!(&tag, b"SQPK", "ChecksumMismatch must carry the SQPK tag");
}
other => panic!("expected ChecksumMismatch, got {other:?}"),
}
}