mod common;
use std::io::Cursor;
use common::{adir_body, wrap_patch};
use zipatch_rs::index::{
FilesystemOp, PartExpected, PartSource, PatchRef, PatchSourceKind, Region, Target, TargetPath,
};
use zipatch_rs::test_utils::make_chunk;
use zipatch_rs::{
ApplyContext, IndexApplier, IndexedCheckpoint, MemoryPatchSource, Plan, Platform,
SequentialCheckpoint, ZiPatchError, ZiPatchReader,
};
fn single_adir_patch() -> Vec<u8> {
wrap_patch(vec![make_chunk(b"ADIR", &adir_body("created"))])
}
fn trivial_plan() -> (Vec<u8>, Plan) {
let payload = b"hello";
let src = payload.to_vec();
let target = Target::new(
TargetPath::Generic("out.bin".into()),
payload.len() as u64,
vec![Region::new(
0,
payload.len() as u32,
PartSource::Patch {
patch_idx: 0,
offset: 0,
kind: PatchSourceKind::Raw {
len: payload.len() as u32,
},
decoded_skip: 0,
},
PartExpected::SizeOnly,
)],
);
let plan = Plan::new(
Platform::Win32,
vec![PatchRef::new("synthetic", None)],
vec![target],
vec![] as Vec<FilesystemOp>,
);
(src, plan)
}
#[test]
fn sequential_schema_version_higher_than_current_is_rejected() {
let patch = single_adir_patch();
let mut bad = SequentialCheckpoint::new(0, 0, Some("x".into()), None, None);
bad.schema_version = SequentialCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_add(1);
let tmp = tempfile::tempdir().unwrap();
let mut ctx = ApplyContext::new(tmp.path());
let err = ZiPatchReader::new(Cursor::new(patch))
.unwrap()
.with_patch_name("x")
.resume_apply_to(&mut ctx, Some(&bad))
.expect_err("higher schema_version must surface as SchemaVersionMismatch");
let ZiPatchError::SchemaVersionMismatch {
kind,
found,
expected,
} = err
else {
panic!("expected SchemaVersionMismatch, got {err:?}");
};
assert_eq!(kind, "sequential-checkpoint");
assert_eq!(
found,
SequentialCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_add(1)
);
assert_eq!(expected, SequentialCheckpoint::CURRENT_SCHEMA_VERSION);
}
#[test]
fn sequential_schema_version_lower_than_current_is_rejected() {
let patch = single_adir_patch();
let mut bad = SequentialCheckpoint::new(0, 0, Some("x".into()), None, None);
bad.schema_version = SequentialCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_sub(1);
let tmp = tempfile::tempdir().unwrap();
let mut ctx = ApplyContext::new(tmp.path());
let err = ZiPatchReader::new(Cursor::new(patch))
.unwrap()
.with_patch_name("x")
.resume_apply_to(&mut ctx, Some(&bad))
.expect_err("lower schema_version must surface as SchemaVersionMismatch");
let ZiPatchError::SchemaVersionMismatch {
kind,
found,
expected,
} = err
else {
panic!("expected SchemaVersionMismatch, got {err:?}");
};
assert_eq!(kind, "sequential-checkpoint");
assert_eq!(
found,
SequentialCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_sub(1)
);
assert_eq!(expected, SequentialCheckpoint::CURRENT_SCHEMA_VERSION);
}
#[test]
fn indexed_schema_version_higher_than_current_is_rejected() {
let (src, plan) = trivial_plan();
let mut bad = IndexedCheckpoint::new(plan.crc32(), false, 0, 0, 0);
bad.schema_version = IndexedCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_add(1);
let tmp = tempfile::tempdir().unwrap();
let err = IndexApplier::new(MemoryPatchSource::new(src), tmp.path())
.resume_execute(&plan, Some(&bad))
.expect_err("higher schema_version must surface as SchemaVersionMismatch");
let ZiPatchError::SchemaVersionMismatch {
kind,
found,
expected,
} = err
else {
panic!("expected SchemaVersionMismatch, got {err:?}");
};
assert_eq!(kind, "indexed-checkpoint");
assert_eq!(
found,
IndexedCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_add(1)
);
assert_eq!(expected, IndexedCheckpoint::CURRENT_SCHEMA_VERSION);
}
#[test]
fn indexed_schema_version_lower_than_current_is_rejected() {
let (src, plan) = trivial_plan();
let mut bad = IndexedCheckpoint::new(plan.crc32(), false, 0, 0, 0);
bad.schema_version = IndexedCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_sub(1);
let tmp = tempfile::tempdir().unwrap();
let err = IndexApplier::new(MemoryPatchSource::new(src), tmp.path())
.resume_execute(&plan, Some(&bad))
.expect_err("lower schema_version must surface as SchemaVersionMismatch");
let ZiPatchError::SchemaVersionMismatch {
kind,
found,
expected,
} = err
else {
panic!("expected SchemaVersionMismatch, got {err:?}");
};
assert_eq!(kind, "indexed-checkpoint");
assert_eq!(
found,
IndexedCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_sub(1)
);
assert_eq!(expected, IndexedCheckpoint::CURRENT_SCHEMA_VERSION);
}
#[test]
fn resume_with_over_advanced_chunk_index_returns_truncated_patch() {
let patch = single_adir_patch();
let too_far = SequentialCheckpoint::new(5, 999, Some("x".into()), None, None);
let tmp = tempfile::tempdir().unwrap();
let mut ctx = ApplyContext::new(tmp.path());
let err = ZiPatchReader::new(Cursor::new(patch))
.unwrap()
.with_patch_name("x")
.resume_apply_to(&mut ctx, Some(&too_far))
.expect_err("next_chunk_index past stream end must return TruncatedPatch");
assert!(
matches!(err, ZiPatchError::TruncatedPatch),
"expected TruncatedPatch, got {err:?}"
);
}
#[test]
fn resume_single_chunk_patch_with_none_matches_apply_to() {
let patch = single_adir_patch();
let tmp_clean = tempfile::tempdir().unwrap();
ZiPatchReader::new(Cursor::new(patch.clone()))
.unwrap()
.with_patch_name("single.patch")
.apply_to(&mut ApplyContext::new(tmp_clean.path()))
.unwrap();
let tmp_resume = tempfile::tempdir().unwrap();
let final_cp = ZiPatchReader::new(Cursor::new(patch))
.unwrap()
.with_patch_name("single.patch")
.resume_apply_to(&mut ApplyContext::new(tmp_resume.path()), None)
.unwrap();
assert!(tmp_resume.path().join("created").is_dir());
assert_eq!(final_cp.patch_name.as_deref(), Some("single.patch"));
assert_eq!(
final_cp.next_chunk_index, 1,
"single ADIR chunk → index = 1"
);
}