use crate::Platform;
use crate::Result;
use crate::index::apply::decompress_full;
use crate::index::source::PatchSource;
use std::collections::HashMap;
use tracing::{info, info_span, warn};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PatchType {
GameData,
Boot,
Other([u8; 4]),
}
impl PatchType {
#[must_use]
pub fn from_tag(tag: [u8; 4]) -> Self {
match &tag {
b"D000" => PatchType::GameData,
b"H000" => PatchType::Boot,
_ => PatchType::Other(tag),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PatchRef {
pub name: String,
pub patch_type: Option<PatchType>,
}
impl PatchRef {
#[must_use]
pub fn new(name: impl Into<String>, patch_type: Option<PatchType>) -> Self {
Self {
name: name.into(),
patch_type,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TargetPath {
SqpackDat {
main_id: u16,
sub_id: u16,
file_id: u32,
},
SqpackIndex {
main_id: u16,
sub_id: u16,
file_id: u32,
},
Generic(String),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Target {
pub path: TargetPath,
pub final_size: u64,
pub regions: Vec<Region>,
}
impl Target {
#[must_use]
pub fn new(path: TargetPath, final_size: u64, regions: Vec<Region>) -> Self {
Self {
path,
final_size,
regions,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Region {
pub target_offset: u64,
pub length: u32,
pub source: PartSource,
pub expected: PartExpected,
}
impl Region {
#[must_use]
pub fn new(
target_offset: u64,
length: u32,
source: PartSource,
expected: PartExpected,
) -> Self {
Self {
target_offset,
length,
source,
expected,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PartSource {
Patch {
patch_idx: u32,
offset: u64,
kind: PatchSourceKind,
decoded_skip: u16,
},
Zeros,
EmptyBlock {
units: u32,
},
Unavailable,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PatchSourceKind {
Raw {
len: u32,
},
Deflated {
compressed_len: u32,
decompressed_len: u32,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PartExpected {
SizeOnly,
Crc32(u32),
Zeros,
EmptyBlock {
units: u32,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FilesystemOp {
EnsureDir(String),
DeleteDir(String),
DeleteFile(String),
MakeDirTree(String),
RemoveAllInExpansion(u16),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Plan {
#[cfg_attr(feature = "serde", serde(default = "Plan::default_schema_version"))]
pub schema_version: u32,
pub platform: Platform,
pub patches: Vec<PatchRef>,
pub targets: Vec<Target>,
pub fs_ops: Vec<FilesystemOp>,
}
impl Plan {
pub const CURRENT_SCHEMA_VERSION: u32 = 1;
#[cfg(feature = "serde")]
#[must_use]
fn default_schema_version() -> u32 {
Self::CURRENT_SCHEMA_VERSION
}
pub fn check_schema_version(&self) -> Result<()> {
if self.schema_version != Self::CURRENT_SCHEMA_VERSION {
return Err(crate::ZiPatchError::SchemaVersionMismatch {
kind: "plan",
found: self.schema_version,
expected: Self::CURRENT_SCHEMA_VERSION,
});
}
Ok(())
}
#[must_use]
pub fn new(
platform: Platform,
patches: Vec<PatchRef>,
targets: Vec<Target>,
fs_ops: Vec<FilesystemOp>,
) -> Self {
Self {
schema_version: Self::CURRENT_SCHEMA_VERSION,
platform,
patches,
targets,
fs_ops,
}
}
pub fn compute_crc32<S: PatchSource>(&mut self, source: &mut S) -> Result<()> {
let span = info_span!("compute_crc32", targets = self.targets.len());
let _enter = span.enter();
let mut compressed_scratch: Vec<u8> = Vec::new();
let mut decompressed_scratch: Vec<u8> = Vec::new();
let mut decompressor = flate2::Decompress::new(false);
let mut zeros_cache: HashMap<u32, u32> = HashMap::with_capacity(4);
let mut empty_block_cache: HashMap<u32, u32> = HashMap::with_capacity(4);
let mut updates: Vec<(usize, usize, u32)> = Vec::new();
let mut unavailable_skipped: usize = 0;
for (t_idx, target) in self.targets.iter().enumerate() {
for (r_idx, region) in target.regions.iter().enumerate() {
match ®ion.source {
PartSource::Patch {
patch_idx,
offset,
kind,
decoded_skip,
} => {
let crc = patch_region_crc(
source,
*patch_idx,
*offset,
kind,
*decoded_skip,
region.length,
&mut compressed_scratch,
&mut decompressed_scratch,
&mut decompressor,
)?;
updates.push((t_idx, r_idx, crc));
}
PartSource::Zeros => {
let crc = *zeros_cache
.entry(region.length)
.or_insert_with(|| crc32_of_zeros(region.length));
updates.push((t_idx, r_idx, crc));
}
PartSource::EmptyBlock { units } => {
let crc = match empty_block_cache.entry(*units) {
std::collections::hash_map::Entry::Occupied(e) => *e.get(),
std::collections::hash_map::Entry::Vacant(e) => {
let crc = crc32_of_empty_block(*units)?;
*e.insert(crc)
}
};
updates.push((t_idx, r_idx, crc));
}
PartSource::Unavailable => {
unavailable_skipped += 1;
tracing::trace!(
target_idx = t_idx,
region_idx = r_idx,
target_offset = region.target_offset,
length = region.length,
"compute_crc32: skipping Unavailable region"
);
}
}
}
}
let populated = updates.len();
for (t_idx, r_idx, crc) in updates {
self.targets[t_idx].regions[r_idx].expected = PartExpected::Crc32(crc);
}
if unavailable_skipped > 0 {
warn!(
skipped = unavailable_skipped,
"compute_crc32: left Unavailable regions with their existing expected"
);
}
info!(
populated,
skipped = unavailable_skipped,
"compute_crc32: populated CRC32 for regions"
);
Ok(())
}
#[must_use]
pub fn crc32(&self) -> u32 {
let mut hasher = crc32fast::Hasher::new();
plan_feed_crc(self, &mut hasher);
hasher.finalize()
}
}
fn plan_feed_crc(plan: &Plan, h: &mut crc32fast::Hasher) {
h.update(b"zipatch-rs/plan/v1");
h.update(&plan.schema_version.to_le_bytes());
feed_platform(plan.platform, h);
feed_len(plan.patches.len(), h);
for p in &plan.patches {
feed_str(&p.name, h);
feed_patch_type(p.patch_type.as_ref(), h);
}
feed_len(plan.targets.len(), h);
for t in &plan.targets {
feed_target_path(&t.path, h);
h.update(&t.final_size.to_le_bytes());
feed_len(t.regions.len(), h);
for r in &t.regions {
h.update(&r.target_offset.to_le_bytes());
h.update(&r.length.to_le_bytes());
feed_part_source(&r.source, h);
feed_part_expected(&r.expected, h);
}
}
feed_len(plan.fs_ops.len(), h);
for op in &plan.fs_ops {
feed_fs_op(op, h);
}
}
fn feed_len(n: usize, h: &mut crc32fast::Hasher) {
h.update(&(n as u64).to_le_bytes());
}
fn feed_str(s: &str, h: &mut crc32fast::Hasher) {
feed_len(s.len(), h);
h.update(s.as_bytes());
}
fn feed_platform(p: crate::Platform, h: &mut crc32fast::Hasher) {
match p {
crate::Platform::Win32 => h.update(&[0u8]),
crate::Platform::Ps3 => h.update(&[1u8]),
crate::Platform::Ps4 => h.update(&[2u8]),
crate::Platform::Unknown(raw) => {
h.update(&[3u8]);
h.update(&raw.to_le_bytes());
}
}
}
fn feed_patch_type(p: Option<&PatchType>, h: &mut crc32fast::Hasher) {
match p {
None => h.update(&[0u8]),
Some(PatchType::GameData) => h.update(&[1u8]),
Some(PatchType::Boot) => h.update(&[2u8]),
Some(PatchType::Other(tag)) => {
h.update(&[3u8]);
h.update(tag);
}
}
}
fn feed_target_path(tp: &TargetPath, h: &mut crc32fast::Hasher) {
match *tp {
TargetPath::SqpackDat {
main_id,
sub_id,
file_id,
} => {
h.update(&[0u8]);
h.update(&main_id.to_le_bytes());
h.update(&sub_id.to_le_bytes());
h.update(&file_id.to_le_bytes());
}
TargetPath::SqpackIndex {
main_id,
sub_id,
file_id,
} => {
h.update(&[1u8]);
h.update(&main_id.to_le_bytes());
h.update(&sub_id.to_le_bytes());
h.update(&file_id.to_le_bytes());
}
TargetPath::Generic(ref rel) => {
h.update(&[2u8]);
feed_str(rel, h);
}
}
}
fn feed_part_source(s: &PartSource, h: &mut crc32fast::Hasher) {
match *s {
PartSource::Patch {
patch_idx,
offset,
ref kind,
decoded_skip,
} => {
h.update(&[0u8]);
h.update(&patch_idx.to_le_bytes());
h.update(&offset.to_le_bytes());
match *kind {
PatchSourceKind::Raw { len } => {
h.update(&[0u8]);
h.update(&len.to_le_bytes());
}
PatchSourceKind::Deflated {
compressed_len,
decompressed_len,
} => {
h.update(&[1u8]);
h.update(&compressed_len.to_le_bytes());
h.update(&decompressed_len.to_le_bytes());
}
}
h.update(&decoded_skip.to_le_bytes());
}
PartSource::Zeros => h.update(&[1u8]),
PartSource::EmptyBlock { units } => {
h.update(&[2u8]);
h.update(&units.to_le_bytes());
}
PartSource::Unavailable => h.update(&[3u8]),
}
}
fn feed_part_expected(e: &PartExpected, h: &mut crc32fast::Hasher) {
match *e {
PartExpected::SizeOnly => h.update(&[0u8]),
PartExpected::Crc32(c) => {
h.update(&[1u8]);
h.update(&c.to_le_bytes());
}
PartExpected::Zeros => h.update(&[2u8]),
PartExpected::EmptyBlock { units } => {
h.update(&[3u8]);
h.update(&units.to_le_bytes());
}
}
}
fn feed_fs_op(op: &FilesystemOp, h: &mut crc32fast::Hasher) {
match op {
FilesystemOp::EnsureDir(s) => {
h.update(&[0u8]);
feed_str(s, h);
}
FilesystemOp::DeleteDir(s) => {
h.update(&[1u8]);
feed_str(s, h);
}
FilesystemOp::DeleteFile(s) => {
h.update(&[2u8]);
feed_str(s, h);
}
FilesystemOp::MakeDirTree(s) => {
h.update(&[3u8]);
feed_str(s, h);
}
FilesystemOp::RemoveAllInExpansion(id) => {
h.update(&[4u8]);
h.update(&id.to_le_bytes());
}
}
}
#[allow(clippy::too_many_arguments)]
fn patch_region_crc<S: PatchSource>(
source: &mut S,
patch_idx: u32,
offset: u64,
kind: &PatchSourceKind,
decoded_skip: u16,
length: u32,
compressed_scratch: &mut Vec<u8>,
decompressed_scratch: &mut Vec<u8>,
decompressor: &mut flate2::Decompress,
) -> Result<u32> {
match *kind {
PatchSourceKind::Raw { len } => {
let len_us = len as usize;
if compressed_scratch.len() < len_us {
compressed_scratch.resize(len_us, 0);
}
source.read(patch_idx, offset, &mut compressed_scratch[..len_us])?;
Ok(crc32fast::hash(&compressed_scratch[..len_us]))
}
PatchSourceKind::Deflated {
compressed_len,
decompressed_len,
} => {
let comp_us = compressed_len as usize;
if compressed_scratch.len() < comp_us {
compressed_scratch.resize(comp_us, 0);
}
source.read(patch_idx, offset, &mut compressed_scratch[..comp_us])?;
let produced = decompress_full(
decompressor,
&compressed_scratch[..comp_us],
decompressed_len,
decompressed_scratch,
)?;
let skip = decoded_skip as usize;
let end = skip + length as usize;
let clamped_end = end.min(produced);
let clamped_start = skip.min(clamped_end);
Ok(crc32fast::hash(
&decompressed_scratch[clamped_start..clamped_end],
))
}
}
}
fn crc32_of_zeros(length: u32) -> u32 {
static ZERO_BUF: [u8; 64 * 1024] = [0; 64 * 1024];
let mut hasher = crc32fast::Hasher::new();
let mut remaining = length as u64;
while remaining > 0 {
let n = remaining.min(ZERO_BUF.len() as u64) as usize;
hasher.update(&ZERO_BUF[..n]);
remaining -= n as u64;
}
hasher.finalize()
}
fn crc32_of_empty_block(units: u32) -> Result<u32> {
static ZERO_BUF: [u8; 64 * 1024] = [0; 64 * 1024];
if units == 0 {
return Err(crate::ZiPatchError::InvalidField {
context: "EmptyBlock units must be non-zero",
});
}
let mut hasher = crc32fast::Hasher::new();
hasher.update(&crate::apply::sqpk::empty_block_header(units));
let mut remaining = u64::from(units) * 128 - 20;
while remaining > 0 {
let n = remaining.min(ZERO_BUF.len() as u64) as usize;
hasher.update(&ZERO_BUF[..n]);
remaining -= n as u64;
}
Ok(hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn patch_type_from_tag_known_values() {
assert_eq!(PatchType::from_tag(*b"D000"), PatchType::GameData);
assert_eq!(PatchType::from_tag(*b"H000"), PatchType::Boot);
assert_eq!(
PatchType::from_tag(*b"Z999"),
PatchType::Other(*b"Z999"),
"unknown tags must round-trip through Other"
);
}
#[test]
fn part_source_variants_are_clone_partial_eq() {
let a = PartSource::Patch {
patch_idx: 0,
offset: 1024,
kind: PatchSourceKind::Raw { len: 128 },
decoded_skip: 0,
};
let b = a.clone();
assert_eq!(a, b);
let z1 = PartSource::Zeros;
let z2 = PartSource::Zeros;
assert_eq!(z1, z2);
let e1 = PartSource::EmptyBlock { units: 4 };
let e2 = PartSource::EmptyBlock { units: 4 };
assert_eq!(e1, e2);
assert_ne!(e1, PartSource::EmptyBlock { units: 5 });
}
#[test]
fn patch_source_kind_distinguishes_raw_and_deflated() {
let raw = PatchSourceKind::Raw { len: 16 };
let def = PatchSourceKind::Deflated {
compressed_len: 8,
decompressed_len: 16,
};
assert_ne!(raw, def);
assert_ne!(
def,
PatchSourceKind::Deflated {
compressed_len: 8,
decompressed_len: 32,
}
);
}
#[test]
fn part_expected_variants_round_trip() {
let cases = [
PartExpected::SizeOnly,
PartExpected::Crc32(0xDEAD_BEEF),
PartExpected::Zeros,
PartExpected::EmptyBlock { units: 2 },
];
for c in &cases {
assert_eq!(c, &c.clone());
}
}
#[test]
fn filesystem_op_variants_round_trip() {
let ops = [
FilesystemOp::EnsureDir("sqpack/ffxiv".to_owned()),
FilesystemOp::DeleteDir("old".to_owned()),
FilesystemOp::DeleteFile("dead.dat".to_owned()),
FilesystemOp::MakeDirTree("sqpack/ex1".to_owned()),
FilesystemOp::RemoveAllInExpansion(2),
];
for op in &ops {
assert_eq!(op, &op.clone());
}
}
use crate::index::source::MemoryPatchSource;
use flate2::Compression;
use flate2::write::DeflateEncoder;
use std::io::Write;
fn plan_with_regions(regions: Vec<Region>) -> Plan {
Plan {
schema_version: Plan::CURRENT_SCHEMA_VERSION,
platform: Platform::Win32,
patches: vec![PatchRef {
name: "synthetic".into(),
patch_type: None,
}],
targets: vec![Target {
path: TargetPath::Generic("file.bin".into()),
final_size: regions
.last()
.map_or(0, |r| r.target_offset + u64::from(r.length)),
regions,
}],
fs_ops: vec![],
}
}
#[test]
fn compute_crc32_populates_every_patch_region() {
let raw_payload: Vec<u8> = (0..64u8).collect();
let deflate_src: Vec<u8> = (0..128u8).map(|i| i.wrapping_mul(3)).collect();
let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
enc.write_all(&deflate_src).unwrap();
let compressed = enc.finish().unwrap();
let mut source_buf = vec![0u8; 4096];
source_buf[..raw_payload.len()].copy_from_slice(&raw_payload);
source_buf[256..256 + compressed.len()].copy_from_slice(&compressed);
let regions = vec![
Region {
target_offset: 0,
length: raw_payload.len() as u32,
source: PartSource::Patch {
patch_idx: 0,
offset: 0,
kind: PatchSourceKind::Raw {
len: raw_payload.len() as u32,
},
decoded_skip: 0,
},
expected: PartExpected::SizeOnly,
},
Region {
target_offset: raw_payload.len() as u64,
length: deflate_src.len() as u32,
source: PartSource::Patch {
patch_idx: 0,
offset: 256,
kind: PatchSourceKind::Deflated {
compressed_len: compressed.len() as u32,
decompressed_len: deflate_src.len() as u32,
},
decoded_skip: 0,
},
expected: PartExpected::SizeOnly,
},
];
let mut plan = plan_with_regions(regions);
let mut src = MemoryPatchSource::new(source_buf);
plan.compute_crc32(&mut src).expect("compute_crc32");
let raw_expected = crc32fast::hash(&raw_payload);
let def_expected = crc32fast::hash(&deflate_src);
assert_eq!(
plan.targets[0].regions[0].expected,
PartExpected::Crc32(raw_expected)
);
assert_eq!(
plan.targets[0].regions[1].expected,
PartExpected::Crc32(def_expected)
);
}
#[test]
fn compute_crc32_deflated_honors_decoded_skip() {
let payload: Vec<u8> = (0..256u32).map(|i| (i * 11) as u8).collect();
let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
enc.write_all(&payload).unwrap();
let compressed = enc.finish().unwrap();
let mut source_buf = vec![0u8; 4096];
source_buf[100..100 + compressed.len()].copy_from_slice(&compressed);
let regions = vec![Region {
target_offset: 0,
length: 128,
source: PartSource::Patch {
patch_idx: 0,
offset: 100,
kind: PatchSourceKind::Deflated {
compressed_len: compressed.len() as u32,
decompressed_len: payload.len() as u32,
},
decoded_skip: 128,
},
expected: PartExpected::SizeOnly,
}];
let mut plan = plan_with_regions(regions);
let mut src = MemoryPatchSource::new(source_buf);
plan.compute_crc32(&mut src).unwrap();
let expected = crc32fast::hash(&payload[128..256]);
assert_eq!(
plan.targets[0].regions[0].expected,
PartExpected::Crc32(expected)
);
}
#[test]
fn compute_crc32_uses_canonical_zeros() {
let regions = vec![Region {
target_offset: 0,
length: 128,
source: PartSource::Zeros,
expected: PartExpected::Zeros,
}];
let mut plan = plan_with_regions(regions);
let mut src = MemoryPatchSource::new(Vec::new());
plan.compute_crc32(&mut src).unwrap();
let expected = crc32fast::hash(&[0u8; 128]);
assert_eq!(
plan.targets[0].regions[0].expected,
PartExpected::Crc32(expected)
);
}
#[test]
fn compute_crc32_uses_canonical_empty_block() {
let regions = vec![Region {
target_offset: 0,
length: 128,
source: PartSource::EmptyBlock { units: 1 },
expected: PartExpected::EmptyBlock { units: 1 },
}];
let mut plan = plan_with_regions(regions);
let mut src = MemoryPatchSource::new(Vec::new());
plan.compute_crc32(&mut src).unwrap();
let mut buf = Vec::with_capacity(128);
crate::apply::sqpk::write_empty_block(&mut std::io::Cursor::new(&mut buf), 0, 1).unwrap();
let expected = crc32fast::hash(&buf);
assert_eq!(
plan.targets[0].regions[0].expected,
PartExpected::Crc32(expected)
);
}
#[test]
fn compute_crc32_short_stream_after_prior_region_does_not_fold_stale_bytes() {
let first_payload: Vec<u8> = (0..96u8).collect();
let second_payload: &[u8] = b"abcd";
let compress = |raw: &[u8]| {
let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
enc.write_all(raw).unwrap();
enc.finish().unwrap()
};
let first_compressed = compress(&first_payload);
let second_compressed = compress(second_payload);
let mut src_buf = Vec::new();
let first_offset = src_buf.len() as u64;
src_buf.extend_from_slice(&first_compressed);
let second_offset = src_buf.len() as u64;
src_buf.extend_from_slice(&second_compressed);
let declared_second_len: u32 = first_payload.len() as u32;
let regions = vec![
Region {
target_offset: 0,
length: first_payload.len() as u32,
source: PartSource::Patch {
patch_idx: 0,
offset: first_offset,
kind: PatchSourceKind::Deflated {
compressed_len: first_compressed.len() as u32,
decompressed_len: first_payload.len() as u32,
},
decoded_skip: 0,
},
expected: PartExpected::SizeOnly,
},
Region {
target_offset: u64::from(first_payload.len() as u32),
length: declared_second_len,
source: PartSource::Patch {
patch_idx: 0,
offset: second_offset,
kind: PatchSourceKind::Deflated {
compressed_len: second_compressed.len() as u32,
decompressed_len: declared_second_len,
},
decoded_skip: 0,
},
expected: PartExpected::SizeOnly,
},
];
let mut plan = plan_with_regions(regions);
let mut src = MemoryPatchSource::new(src_buf);
plan.compute_crc32(&mut src).unwrap();
let expected_second = crc32fast::hash(second_payload);
let leaked_second = {
let mut buf = Vec::with_capacity(declared_second_len as usize);
buf.extend_from_slice(second_payload);
buf.extend_from_slice(&first_payload[second_payload.len()..]);
crc32fast::hash(&buf)
};
let got = match &plan.targets[0].regions[1].expected {
PartExpected::Crc32(c) => *c,
other => panic!("expected Crc32, got {other:?}"),
};
assert_eq!(
got, expected_second,
"CRC must be of decoded bytes only (got {got:#x}, expected {expected_second:#x})"
);
assert_ne!(
got, leaked_second,
"CRC must not fold in stale scratch bytes from a prior region"
);
}
#[test]
fn compute_crc32_rolls_back_on_midway_source_failure() {
let payload: Vec<u8> = (0..32u8).collect();
let mut src_buf = vec![0u8; 64];
src_buf[..payload.len()].copy_from_slice(&payload);
let plan_targets = vec![
Target {
path: TargetPath::Generic("a.bin".into()),
final_size: payload.len() as u64,
regions: vec![Region {
target_offset: 0,
length: payload.len() as u32,
source: PartSource::Patch {
patch_idx: 0,
offset: 0,
kind: PatchSourceKind::Raw {
len: payload.len() as u32,
},
decoded_skip: 0,
},
expected: PartExpected::SizeOnly,
}],
},
Target {
path: TargetPath::Generic("b.bin".into()),
final_size: 4096,
regions: vec![Region {
target_offset: 0,
length: 32,
source: PartSource::Patch {
patch_idx: 0,
offset: 4096,
kind: PatchSourceKind::Raw { len: 32 },
decoded_skip: 0,
},
expected: PartExpected::SizeOnly,
}],
},
];
let mut plan = Plan {
schema_version: Plan::CURRENT_SCHEMA_VERSION,
platform: Platform::Win32,
patches: vec![PatchRef {
name: "synthetic".into(),
patch_type: None,
}],
targets: plan_targets,
fs_ops: vec![],
};
let mut src = MemoryPatchSource::new(src_buf);
let err = plan
.compute_crc32(&mut src)
.expect_err("second target's read must fail");
assert!(
matches!(err, crate::ZiPatchError::PatchSourceTooShort { .. }),
"expected PatchSourceTooShort, got {err:?}"
);
for target in &plan.targets {
for region in &target.regions {
assert_eq!(
region.expected,
PartExpected::SizeOnly,
"Err return must leave plan unmutated, but a region was set to {:?}",
region.expected
);
}
}
}
#[test]
fn crc32_of_empty_block_matches_explicit_buffer() {
for units in [1u32, 2, 4, 8, 16, 100, 1024, 8192] {
let mut buf = vec![0u8; (units as usize) * 128];
buf[0..4].copy_from_slice(&128u32.to_le_bytes());
buf[12..16].copy_from_slice(&units.wrapping_sub(1).to_le_bytes());
let expected = crc32fast::hash(&buf);
let got = crc32_of_empty_block(units).unwrap();
assert_eq!(got, expected, "units={units}");
}
}
#[test]
fn crc32_of_empty_block_rejects_zero_units() {
let err = crc32_of_empty_block(0).unwrap_err();
assert!(
matches!(err, crate::ZiPatchError::InvalidField { context } if context.contains("non-zero")),
"got {err:?}"
);
}
#[test]
fn compute_crc32_skips_unavailable_regions() {
let regions = vec![Region {
target_offset: 0,
length: 32,
source: PartSource::Unavailable,
expected: PartExpected::SizeOnly,
}];
let mut plan = plan_with_regions(regions);
let mut src = MemoryPatchSource::new(Vec::new());
plan.compute_crc32(&mut src)
.expect("must not error on Unavailable");
assert_eq!(plan.targets[0].regions[0].expected, PartExpected::SizeOnly);
}
}