mod common;
use std::sync::Arc;
use common::assert_trees_equal;
use zipatch_rs::index::{
FilesystemOp, PartExpected, PartSource, PatchRef, PatchSourceKind, Region, Target, TargetPath,
};
use zipatch_rs::{
Checkpoint, IndexApplier, IndexedCheckpoint, MemoryPatchSource, Plan, Platform, ZiPatchError,
};
fn raw_region(target_offset: u64, len: u32, src_offset: u64) -> Region {
Region::new(
target_offset,
len,
PartSource::Patch {
patch_idx: 0,
offset: src_offset,
kind: PatchSourceKind::Raw { len },
decoded_skip: 0,
},
PartExpected::SizeOnly,
)
}
fn generic_target(rel: &str, regions: Vec<Region>) -> Target {
let final_size = regions
.last()
.map_or(0, |r| r.target_offset + u64::from(r.length));
Target::new(TargetPath::Generic(rel.to_owned()), final_size, regions)
}
fn build_multi_target_plan() -> (Vec<u8>, Plan) {
let mut buf: Vec<u8> = Vec::new();
let a_regions: Vec<Region> = (0..4)
.map(|i| {
let pat = [(0x10u8 + i as u8); 32];
let off = buf.len() as u64;
buf.extend_from_slice(&pat);
raw_region(i as u64 * 32, 32, off)
})
.collect();
let b_regions: Vec<Region> = (0..70)
.map(|i| {
let pat = [(0x80u8 | (i as u8)); 16];
let off = buf.len() as u64;
buf.extend_from_slice(&pat);
raw_region(i as u64 * 16, 16, off)
})
.collect();
let c_regions: Vec<Region> = (0..3)
.map(|i| {
let pat = [(0xC0u8 + i as u8); 24];
let off = buf.len() as u64;
buf.extend_from_slice(&pat);
raw_region(i as u64 * 24, 24, off)
})
.collect();
let plan = Plan::new(
Platform::Win32,
vec![PatchRef::new("synthetic", None)],
vec![
generic_target("a.bin", a_regions),
generic_target("b.bin", b_regions),
generic_target("c.bin", c_regions),
],
vec![FilesystemOp::MakeDirTree("staging".into())],
);
(buf, plan)
}
#[derive(Clone, Default)]
struct CaptureSink(Arc<std::sync::Mutex<Vec<Checkpoint>>>);
impl CaptureSink {
fn new() -> Self {
Self::default()
}
fn records(&self) -> Vec<Checkpoint> {
self.0.lock().unwrap().clone()
}
}
impl zipatch_rs::CheckpointSink for CaptureSink {
fn record(&mut self, c: &Checkpoint) -> std::io::Result<()> {
self.0.lock().unwrap().push(c.clone());
Ok(())
}
}
#[test]
fn resume_execute_with_none_matches_execute_byte_for_byte() {
let (src_buf, plan) = build_multi_target_plan();
let tmp_clean = tempfile::tempdir().unwrap();
IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_clean.path())
.execute(&plan)
.unwrap();
let tmp_resume = tempfile::tempdir().unwrap();
let final_cp = IndexApplier::new(MemoryPatchSource::new(src_buf), tmp_resume.path())
.resume_execute(&plan, None)
.unwrap();
assert_trees_equal(tmp_clean.path(), tmp_resume.path());
assert_eq!(final_cp.next_target_idx, plan.targets.len() as u64);
assert_eq!(final_cp.next_region_idx, 0);
assert!(final_cp.fs_ops_done);
assert_eq!(final_cp.plan_crc32, plan.crc32());
}
#[test]
fn resume_mid_target_produces_byte_identical_install() {
let (src_buf, plan) = build_multi_target_plan();
let tmp_clean = tempfile::tempdir().unwrap();
IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_clean.path())
.execute(&plan)
.unwrap();
let sink = CaptureSink::new();
let tmp_partial = tempfile::tempdir().unwrap();
IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_partial.path())
.with_checkpoint_sink(sink.clone())
.execute(&plan)
.unwrap();
let records = sink.records();
let mid_b = records
.iter()
.find_map(|c| match c {
Checkpoint::Indexed(i) if i.next_target_idx == 1 && i.next_region_idx == 64 => Some(i),
_ => None,
})
.expect("a (1, 64) checkpoint must have been emitted")
.clone();
let final_cp = IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_partial.path())
.resume_execute(&plan, Some(&mid_b))
.unwrap();
assert_eq!(final_cp.plan_crc32, plan.crc32());
assert_trees_equal(tmp_clean.path(), tmp_partial.path());
}
#[test]
fn resume_with_fs_ops_done_skips_fs_ops() {
let (src_buf, plan) = build_multi_target_plan();
let tmp = tempfile::tempdir().unwrap();
let cp = IndexedCheckpoint::new(plan.crc32(), true, plan.targets.len() as u64, 0, 0);
IndexApplier::new(MemoryPatchSource::new(src_buf), tmp.path())
.resume_execute(&plan, Some(&cp))
.unwrap();
assert!(
!tmp.path().join("staging").exists(),
"fs_ops_done=true must skip the MakeDirTree fs_op"
);
}
#[test]
fn resume_with_fs_ops_not_done_runs_fs_ops() {
let (src_buf, plan) = build_multi_target_plan();
let tmp = tempfile::tempdir().unwrap();
let cp = IndexedCheckpoint::new(plan.crc32(), false, plan.targets.len() as u64, 0, 0);
IndexApplier::new(MemoryPatchSource::new(src_buf), tmp.path())
.resume_execute(&plan, Some(&cp))
.unwrap();
assert!(
tmp.path().join("staging").is_dir(),
"fs_ops_done=false must run the MakeDirTree fs_op"
);
}
#[test]
fn resume_crc_mismatch_restarts_from_scratch() {
let (src_buf, plan) = build_multi_target_plan();
let wrong_cp = IndexedCheckpoint::new(
plan.crc32().wrapping_add(1),
true,
plan.targets.len() as u64,
0,
0,
);
let tmp_resume = tempfile::tempdir().unwrap();
let final_cp = IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_resume.path())
.resume_execute(&plan, Some(&wrong_cp))
.unwrap();
let tmp_clean = tempfile::tempdir().unwrap();
IndexApplier::new(MemoryPatchSource::new(src_buf), tmp_clean.path())
.execute(&plan)
.unwrap();
assert_trees_equal(tmp_clean.path(), tmp_resume.path());
assert_eq!(final_cp.plan_crc32, plan.crc32());
}
#[test]
fn resume_after_first_target_skips_target_zero() {
let (src_buf, plan) = build_multi_target_plan();
let sink = CaptureSink::new();
let tmp_capture = tempfile::tempdir().unwrap();
IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_capture.path())
.with_checkpoint_sink(sink.clone())
.execute(&plan)
.unwrap();
let records = sink.records();
let boundary = records
.iter()
.find_map(|c| match c {
Checkpoint::Indexed(i) if i.next_target_idx == 1 && i.next_region_idx == 0 => Some(i),
_ => None,
})
.expect("(1, 0) checkpoint must exist")
.clone();
let tmp_resume = tempfile::tempdir().unwrap();
std::fs::copy(
tmp_capture.path().join("a.bin"),
tmp_resume.path().join("a.bin"),
)
.unwrap();
let final_cp = IndexApplier::new(MemoryPatchSource::new(src_buf), tmp_resume.path())
.resume_execute(&plan, Some(&boundary))
.unwrap();
assert_eq!(final_cp.next_target_idx, plan.targets.len() as u64);
assert_trees_equal(tmp_capture.path(), tmp_resume.path());
}
#[test]
fn schema_version_mismatch_rejects_indexed_checkpoint_at_entry() {
let (src_buf, plan) = build_multi_target_plan();
let mut bad = IndexedCheckpoint::new(plan.crc32(), true, 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_buf), tmp.path())
.resume_execute(&plan, Some(&bad))
.expect_err("schema-version mismatch must surface as a typed error");
match err {
ZiPatchError::SchemaVersionMismatch {
kind,
found,
expected,
} => {
assert_eq!(kind, "indexed-checkpoint");
assert_eq!(
found,
IndexedCheckpoint::CURRENT_SCHEMA_VERSION.wrapping_add(1)
);
assert_eq!(expected, IndexedCheckpoint::CURRENT_SCHEMA_VERSION);
}
other => panic!("expected SchemaVersionMismatch, got {other:?}"),
}
}
#[test]
fn resume_stale_crc_with_partial_progress_warns_and_restarts_full_apply() {
let (src_buf, plan) = build_multi_target_plan();
let stale = IndexedCheckpoint::new(plan.crc32().wrapping_add(0xDEAD_BEEF), false, 1, 0, 0);
let tmp_resume = tempfile::tempdir().unwrap();
let final_cp = IndexApplier::new(MemoryPatchSource::new(src_buf.clone()), tmp_resume.path())
.resume_execute(&plan, Some(&stale))
.unwrap();
let tmp_clean = tempfile::tempdir().unwrap();
IndexApplier::new(MemoryPatchSource::new(src_buf), tmp_clean.path())
.execute(&plan)
.unwrap();
assert!(
tmp_resume.path().join("staging").is_dir(),
"CRC mismatch must re-run fs_ops despite the checkpoint claiming progress",
);
assert_trees_equal(tmp_clean.path(), tmp_resume.path());
assert_eq!(final_cp.plan_crc32, plan.crc32());
assert!(final_cp.fs_ops_done);
assert_eq!(final_cp.next_target_idx, plan.targets.len() as u64);
}