use zipatch_rs::chunk::{SqpackFileId, SqpkCommand, SqpkCompressedBlock};
use zipatch_rs::test_utils::wire::{
AddDirectory, ApplyFreeSpace, ApplyOption, ApplyOptionKind, DeleteDirectory, FileHeader,
FileHeaderV2, SqpkAddData, SqpkDeleteData, SqpkExpandData, SqpkFile, SqpkFileOperation,
SqpkHeader, SqpkHeaderTarget, SqpkTargetInfo, TargetFileKind, TargetHeaderKind,
};
use zipatch_rs::{ApplyConfig, ApplyError, ApplySession, Chunk, Platform};
fn ctx(path: impl Into<std::path::PathBuf>) -> ApplySession {
ApplyConfig::new(path).into_session()
}
fn ctx_ignore_missing(path: impl Into<std::path::PathBuf>) -> ApplySession {
ApplyConfig::new(path)
.with_ignore_missing(true)
.into_session()
}
fn dummy_dat() -> SqpackFileId {
SqpackFileId {
main_id: 0,
sub_id: 0,
file_id: 0,
}
}
fn target_info_cmd(platform_id: u16) -> SqpkCommand {
SqpkCommand::TargetInfo(SqpkTargetInfo {
platform_id,
region: 0,
is_debug: false,
version: 0,
deleted_data_size: 0,
seek_count: 0,
})
}
fn sqpk_file(op: SqpkFileOperation, path: &str, file_offset: u64) -> SqpkCommand {
SqpkCommand::File(Box::new(SqpkFile {
operation: op,
file_offset,
file_size: 0,
expansion_id: 0,
path: path.to_string(),
block_source_offsets: vec![],
blocks: vec![],
}))
}
#[test]
fn apply_option_sets_ignore_missing() {
let mut c = ctx("/irrelevant");
Chunk::ApplyOption(ApplyOption {
kind: ApplyOptionKind::IgnoreMissing,
value: true,
})
.apply(&mut c)
.unwrap();
assert!(c.ignore_missing());
Chunk::ApplyOption(ApplyOption {
kind: ApplyOptionKind::IgnoreMissing,
value: false,
})
.apply(&mut c)
.unwrap();
assert!(!c.ignore_missing());
}
#[test]
fn apply_option_sets_ignore_old_mismatch() {
let mut c = ctx("/irrelevant");
Chunk::ApplyOption(ApplyOption {
kind: ApplyOptionKind::IgnoreOldMismatch,
value: true,
})
.apply(&mut c)
.unwrap();
assert!(c.ignore_old_mismatch());
Chunk::ApplyOption(ApplyOption {
kind: ApplyOptionKind::IgnoreOldMismatch,
value: false,
})
.apply(&mut c)
.unwrap();
assert!(!c.ignore_old_mismatch());
}
#[test]
fn add_directory_creates_dir() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
Chunk::AddDirectory(AddDirectory {
name: "sqpack/ffxiv".into(),
})
.apply(&mut c)
.unwrap();
assert!(tmp.path().join("sqpack/ffxiv").is_dir());
}
#[test]
fn delete_directory_not_found_ignore_missing() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx_ignore_missing(tmp.path());
assert!(
Chunk::DeleteDirectory(DeleteDirectory {
name: "does_not_exist".into()
})
.apply(&mut c)
.is_ok()
);
}
#[test]
fn delete_directory_not_found_propagates_error() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
assert!(
Chunk::DeleteDirectory(DeleteDirectory {
name: "does_not_exist".into()
})
.apply(&mut c)
.is_err()
);
}
#[test]
fn target_info_sets_platform_win32() {
let mut c = ctx("/irrelevant");
target_info_cmd(0).apply(&mut c).unwrap();
assert_eq!(c.platform(), Platform::Win32);
}
#[test]
fn target_info_sets_platform_ps3() {
let mut c = ctx("/irrelevant");
target_info_cmd(1).apply(&mut c).unwrap();
assert_eq!(c.platform(), Platform::Ps3);
}
#[test]
fn target_info_sets_platform_ps4() {
let mut c = ctx("/irrelevant");
target_info_cmd(2).apply(&mut c).unwrap();
assert_eq!(c.platform(), Platform::Ps4);
}
#[test]
fn target_info_unknown_platform_is_stored() {
let mut c = ctx("/irrelevant");
target_info_cmd(3).apply(&mut c).unwrap();
assert_eq!(c.platform(), Platform::Unknown(3));
}
#[test]
fn add_data_after_unknown_target_info_returns_unsupported_platform() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
target_info_cmd(99).apply(&mut c).unwrap();
assert_eq!(c.platform(), Platform::Unknown(99));
let err = SqpkCommand::AddData(Box::new(SqpkAddData {
target_file: dummy_dat(),
block_offset: 0,
data_bytes: 1,
block_delete_number: 0,
data: vec![0xAA],
}))
.apply(&mut c)
.expect_err("AddData against an unknown platform must abort path resolution");
match err {
ApplyError::UnsupportedPlatform(id) => assert_eq!(
id, 99,
"error must carry the raw platform_id (99) for diagnostics"
),
other => panic!("expected UnsupportedPlatform(99), got {other:?}"),
}
assert!(
!tmp.path().join("sqpack/ffxiv/000000.win32.dat0").exists(),
"no silent fallback to win32 layout"
);
}
#[test]
fn header_after_unknown_target_info_returns_unsupported_platform_for_index_target() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
target_info_cmd(7).apply(&mut c).unwrap();
let err = SqpkCommand::Header(SqpkHeader {
file_kind: TargetFileKind::Index,
header_kind: TargetHeaderKind::Version,
target: SqpkHeaderTarget::Index(dummy_dat()),
header_data: vec![0; 1024],
})
.apply(&mut c)
.expect_err("Header against an unknown platform must abort path resolution");
match err {
ApplyError::UnsupportedPlatform(7) => {}
other => panic!("expected UnsupportedPlatform(7), got {other:?}"),
}
}
#[test]
fn add_file_after_unknown_target_info_still_succeeds() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
target_info_cmd(42).apply(&mut c).unwrap();
assert_eq!(c.platform(), Platform::Unknown(42));
SqpkCommand::File(Box::new(SqpkFile {
operation: SqpkFileOperation::AddFile,
file_offset: 0,
file_size: 5,
expansion_id: 0,
path: "movie/intro.bk2".to_string(),
block_source_offsets: vec![0],
blocks: vec![SqpkCompressedBlock::new(false, 5, b"hello".to_vec())],
}))
.apply(&mut c)
.expect("generic_path is platform-independent; AddFile must still apply");
c.flush().unwrap();
assert_eq!(
std::fs::read(tmp.path().join("movie/intro.bk2")).unwrap(),
b"hello"
);
}
#[test]
fn add_data_writes_at_offset() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::AddData(Box::new(SqpkAddData {
target_file: dummy_dat(),
block_offset: 4,
data_bytes: 3,
block_delete_number: 2,
data: vec![0xAA, 0xBB, 0xCC],
}))
.apply(&mut c)
.unwrap();
c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.dat0")).unwrap();
assert_eq!(&bytes[0..4], &[0u8; 4]);
assert_eq!(&bytes[4..7], &[0xAA, 0xBB, 0xCC]);
assert_eq!(&bytes[7..9], &[0x00, 0x00]);
}
#[test]
fn open_cached_reuses_handle() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
let make_cmd = |offset: u64, byte: u8| {
SqpkCommand::AddData(Box::new(SqpkAddData {
target_file: dummy_dat(),
block_offset: offset,
data_bytes: 1,
block_delete_number: 0,
data: vec![byte],
}))
};
make_cmd(0, 0xAA).apply(&mut c).unwrap(); make_cmd(1, 0xBB).apply(&mut c).unwrap(); c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.dat0")).unwrap();
assert_eq!(bytes[0], 0xAA);
assert_eq!(bytes[1], 0xBB);
}
#[test]
fn delete_data_zeroes_block() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::DeleteData(SqpkDeleteData {
target_file: dummy_dat(),
block_offset: 0,
block_count: 1,
})
.apply(&mut c)
.unwrap();
c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.dat0")).unwrap();
assert_eq!(bytes.len(), 128); assert_eq!(&bytes[0..4], &128u32.to_le_bytes()); }
#[test]
fn expand_data_zeroes_block() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::ExpandData(SqpkExpandData {
target_file: dummy_dat(),
block_offset: 0,
block_count: 1,
})
.apply(&mut c)
.unwrap();
c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.dat0")).unwrap();
assert_eq!(bytes.len(), 128);
assert_eq!(&bytes[0..4], &128u32.to_le_bytes());
}
#[test]
fn add_file_writes_decompressed_blocks() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::File(Box::new(SqpkFile {
operation: SqpkFileOperation::AddFile,
file_offset: 0,
file_size: 5,
expansion_id: 0,
path: "out.dat".to_string(),
block_source_offsets: vec![0],
blocks: vec![SqpkCompressedBlock::new(false, 5, b"hello".to_vec())],
}))
.apply(&mut c)
.unwrap();
c.flush().unwrap();
assert_eq!(std::fs::read(tmp.path().join("out.dat")).unwrap(), b"hello");
}
#[test]
fn header_version_writes_at_offset_0() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::Header(SqpkHeader {
file_kind: TargetFileKind::Dat,
header_kind: TargetHeaderKind::Version,
target: SqpkHeaderTarget::Dat(dummy_dat()),
header_data: vec![0xABu8; 1024],
})
.apply(&mut c)
.unwrap();
c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.dat0")).unwrap();
assert_eq!(bytes.len(), 1024);
assert!(bytes.iter().all(|&b| b == 0xAB));
}
#[test]
fn header_non_version_writes_at_offset_1024() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::Header(SqpkHeader {
file_kind: TargetFileKind::Dat,
header_kind: TargetHeaderKind::Index,
target: SqpkHeaderTarget::Dat(dummy_dat()),
header_data: vec![0xABu8; 1024],
})
.apply(&mut c)
.unwrap();
c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.dat0")).unwrap();
assert_eq!(bytes.len(), 2048);
assert!(bytes[..1024].iter().all(|&b| b == 0));
assert!(bytes[1024..].iter().all(|&b| b == 0xAB));
}
#[test]
fn add_file_truncates_at_zero_offset() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("out.dat");
std::fs::write(&path, vec![0xFFu8; 100]).unwrap();
let mut c = ctx(tmp.path());
sqpk_file(SqpkFileOperation::AddFile, "out.dat", 0)
.apply(&mut c)
.unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
#[test]
fn add_file_does_not_truncate_at_nonzero_offset() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("out.dat");
std::fs::write(&path, vec![0xFFu8; 100]).unwrap();
let mut c = ctx(tmp.path());
sqpk_file(SqpkFileOperation::AddFile, "out.dat", 10)
.apply(&mut c)
.unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 100);
}
#[test]
fn delete_file_not_found_ignore_missing() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx_ignore_missing(tmp.path());
assert!(
sqpk_file(SqpkFileOperation::DeleteFile, "nonexistent.dat", 0)
.apply(&mut c)
.is_ok()
);
}
#[test]
fn delete_file_not_found_propagates_error() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
assert!(
sqpk_file(SqpkFileOperation::DeleteFile, "nonexistent.dat", 0)
.apply(&mut c)
.is_err()
);
}
#[test]
fn make_dir_tree_creates_directory() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
sqpk_file(SqpkFileOperation::MakeDirTree, "sqpack/ex1", 0)
.apply(&mut c)
.unwrap();
assert!(tmp.path().join("sqpack/ex1").is_dir());
}
#[test]
fn delete_file_succeeds() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("deleteme.dat");
std::fs::write(&path, b"x").unwrap();
let mut c = ctx(tmp.path());
sqpk_file(SqpkFileOperation::DeleteFile, "deleteme.dat", 0)
.apply(&mut c)
.unwrap();
assert!(!path.exists());
}
#[test]
fn header_index_target_writes_index_file() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("sqpack/ffxiv")).unwrap();
let mut c = ctx(tmp.path());
SqpkCommand::Header(SqpkHeader {
file_kind: TargetFileKind::Index,
header_kind: TargetHeaderKind::Version,
target: SqpkHeaderTarget::Index(dummy_dat()),
header_data: vec![0xDDu8; 1024],
})
.apply(&mut c)
.unwrap();
c.flush().unwrap();
let bytes = std::fs::read(tmp.path().join("sqpack/ffxiv/000000.win32.index")).unwrap();
assert_eq!(bytes.len(), 1024);
assert!(bytes.iter().all(|&b| b == 0xDD));
}
#[test]
fn chunk_apply_file_header_noop() {
let mut c = ctx("/irrelevant");
Chunk::FileHeader(FileHeader::V2(FileHeaderV2 {
patch_type: *b"D000",
entry_files: 0,
}))
.apply(&mut c)
.unwrap();
}
#[test]
fn chunk_apply_free_space_noop() {
let mut c = ctx("/irrelevant");
Chunk::ApplyFreeSpace(ApplyFreeSpace {
unknown_a: 0,
unknown_b: 0,
})
.apply(&mut c)
.unwrap();
}
#[test]
fn chunk_apply_eof_noop() {
let mut c = ctx("/irrelevant");
Chunk::EndOfFile.apply(&mut c).unwrap();
}
#[test]
fn chunk_apply_sqpk_dispatches() {
let mut c = ctx("/irrelevant");
Chunk::Sqpk(SqpkCommand::TargetInfo(SqpkTargetInfo {
platform_id: 2,
region: -1,
is_debug: false,
version: 0,
deleted_data_size: 0,
seek_count: 0,
}))
.apply(&mut c)
.unwrap();
assert_eq!(c.platform(), Platform::Ps4);
}
#[test]
fn chunk_apply_option_dispatches() {
let mut c = ctx("/irrelevant");
Chunk::ApplyOption(ApplyOption {
kind: ApplyOptionKind::IgnoreMissing,
value: true,
})
.apply(&mut c)
.unwrap();
assert!(c.ignore_missing());
}
#[test]
fn chunk_apply_add_directory_dispatches() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx(tmp.path());
Chunk::AddDirectory(AddDirectory {
name: "newdir".into(),
})
.apply(&mut c)
.unwrap();
assert!(tmp.path().join("newdir").is_dir());
}
#[test]
fn chunk_apply_delete_directory_dispatches() {
let tmp = tempfile::tempdir().unwrap();
let mut c = ctx_ignore_missing(tmp.path());
Chunk::DeleteDirectory(DeleteDirectory {
name: "nonexistent".into(),
})
.apply(&mut c)
.unwrap();
}
#[test]
fn remove_all_deletes_files_and_keeps_filter() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("sqpack/ffxiv");
std::fs::create_dir_all(&dir).unwrap();
let deleted = ["040100.win32.dat0", "040100.win32.index", "00004.bk2"];
let kept = ["something.var", "00000.bk2"];
for name in deleted.iter().chain(kept.iter()) {
std::fs::write(dir.join(name), b"x").unwrap();
}
let mut c = ctx(tmp.path());
SqpkCommand::File(Box::new(SqpkFile {
operation: SqpkFileOperation::RemoveAll,
file_offset: 0,
file_size: 0,
expansion_id: 0,
path: String::new(),
block_source_offsets: vec![],
blocks: vec![],
}))
.apply(&mut c)
.unwrap();
for name in &deleted {
assert!(!dir.join(name).exists(), "{name} should have been deleted");
}
for name in &kept {
assert!(dir.join(name).exists(), "{name} should have been kept");
}
}