use crate::object::MkitError;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DeltaCorruption {
#[error("declared base_len {declared} does not match the actual base length {actual}")]
BaseLenMismatch { declared: u32, actual: usize },
#[error("COPY opcode {0:#04x} has reserved low bits set")]
ReservedOpcodeBits(u8),
#[error("opcode 0x00 is reserved and must not appear in the instruction stream")]
ZeroOpcode,
#[error("COPY opcode declared a zero-length copy")]
ZeroLengthCopy,
#[error("COPY offset {offset} + length {length} exceeds base_len {base_len} (or overflowed)")]
CopyPastBase {
offset: u32,
length: u16,
base_len: usize,
},
#[error(
"emitting {requested} more byte(s) after {emitted} would exceed the declared result_len {result_len}"
)]
ResultLenOverrun {
emitted: usize,
requested: usize,
result_len: usize,
},
#[error("stream ended after emitting {actual} byte(s), declared result_len is {expected}")]
ResultLenUnderrun { expected: usize, actual: usize },
}
fn corrupt(kind: DeltaCorruption) -> MkitError {
MkitError::DeltaCorrupt(kind)
}
pub const STREAM_VERSION: u8 = 0x01;
pub const OP_COPY: u8 = 0x80;
pub const MAX_INSERT_LEN: usize = 127;
pub const HEADER_LEN: usize = 1 + 4 + 4;
const BLOCK_SIZE: usize = 16;
type FxIndexMap = std::collections::HashMap<u64, u32, rustc_hash::FxSeededState>;
fn random_seed() -> usize {
use std::hash::{BuildHasher, Hasher};
#[allow(clippy::cast_possible_truncation)]
let seed = std::collections::hash_map::RandomState::new()
.build_hasher()
.finish() as usize;
seed
}
pub(crate) const CAP_MULTIPLIER: usize = 256;
#[inline]
pub(crate) fn compute_cap_hint(result_len: usize, _base_len: usize, stream_len: usize) -> usize {
result_len.min(stream_len.saturating_mul(CAP_MULTIPLIER))
}
pub fn encode(base: &[u8], result: &[u8]) -> Result<Vec<u8>, MkitError> {
check_length_bounds(base.len(), result.len())?;
let mut out = Vec::with_capacity(HEADER_LEN + result.len());
write_header(&mut out, base.len(), result.len());
let num_blocks = base.len() / BLOCK_SIZE;
let mut index: FxIndexMap = FxIndexMap::with_capacity_and_hasher(
num_blocks,
rustc_hash::FxSeededState::with_seed(random_seed()),
);
for i in 0..num_blocks {
let pos = i * BLOCK_SIZE;
if let Ok(pos_u32) = u32::try_from(pos) {
let block = &base[pos..pos + BLOCK_SIZE];
let h = block_hash(block);
index.entry(h).or_insert(pos_u32);
} else {
break; }
}
let mut insert_buf: Vec<u8> = Vec::with_capacity(MAX_INSERT_LEN);
let mut ti = 0usize;
let mut window_hash: Option<u64> = None;
while ti < result.len() {
let mut matched = false;
if ti + BLOCK_SIZE <= result.len() {
let target_block = &result[ti..ti + BLOCK_SIZE];
let h = *window_hash.get_or_insert_with(|| block_hash(target_block));
if let Some(&base_pos) = index.get(&h) {
let base_pos_usize = base_pos as usize;
if &base[base_pos_usize..base_pos_usize + BLOCK_SIZE] == target_block {
flush_insert(&mut out, &mut insert_buf);
let mut match_len = BLOCK_SIZE;
while base_pos_usize + match_len < base.len()
&& ti + match_len < result.len()
&& base[base_pos_usize + match_len] == result[ti + match_len]
&& match_len < u16::MAX as usize
{
match_len += 1;
}
emit_copy(
&mut out,
base_pos,
u16::try_from(match_len).expect("<= u16::MAX"),
);
ti += match_len;
window_hash = None;
matched = true;
}
}
} else {
window_hash = None;
}
if !matched {
insert_buf.push(result[ti]);
window_hash = window_hash.and_then(|h| {
(ti + BLOCK_SIZE < result.len())
.then(|| roll_forward(h, result[ti], result[ti + BLOCK_SIZE]))
});
ti += 1;
if insert_buf.len() == MAX_INSERT_LEN {
flush_insert(&mut out, &mut insert_buf);
}
}
}
flush_insert(&mut out, &mut insert_buf);
Ok(out)
}
pub(crate) fn check_length_bounds(base_len: usize, result_len: usize) -> Result<(), MkitError> {
if u32::try_from(base_len).is_err() {
return Err(MkitError::DeltaLengthOverflow {
field: "base_len",
len: base_len,
});
}
if u32::try_from(result_len).is_err() {
return Err(MkitError::DeltaLengthOverflow {
field: "result_len",
len: result_len,
});
}
Ok(())
}
pub fn decode(base: &[u8], stream: &[u8]) -> Result<Vec<u8>, MkitError> {
decode_inner(base, stream, None)
}
pub(crate) fn decode_preallocated(
base: &[u8],
stream: &[u8],
output: Vec<u8>,
) -> Result<Vec<u8>, MkitError> {
decode_inner(base, stream, Some(output))
}
fn decode_inner(base: &[u8], stream: &[u8], output: Option<Vec<u8>>) -> Result<Vec<u8>, MkitError> {
if stream.len() < HEADER_LEN {
return Err(MkitError::UnexpectedEof);
}
if stream[0] != STREAM_VERSION {
return Err(MkitError::UnsupportedObjectVersion);
}
let base_len = u32::from_le_bytes(stream[1..5].try_into().expect("4 bytes")) as usize;
let result_len = u32::from_le_bytes(stream[5..9].try_into().expect("4 bytes")) as usize;
if base_len != base.len() {
#[allow(clippy::cast_possible_truncation)]
let declared = base_len as u32;
return Err(corrupt(DeltaCorruption::BaseLenMismatch {
declared,
actual: base.len(),
}));
}
let cap_hint = compute_cap_hint(result_len, base.len(), stream.len());
let mut out = output.unwrap_or_else(|| Vec::with_capacity(cap_hint));
let mut pos = HEADER_LEN;
while pos < stream.len() {
let op = stream[pos];
pos += 1;
if op & 0x80 != 0 {
if op & 0x7F != 0 {
return Err(corrupt(DeltaCorruption::ReservedOpcodeBits(op)));
}
if pos + 6 > stream.len() {
return Err(MkitError::UnexpectedEof);
}
let offset_u32 = u32::from_le_bytes(stream[pos..pos + 4].try_into().expect("4 bytes"));
let offset = offset_u32 as usize;
pos += 4;
let length_u16 = u16::from_le_bytes(stream[pos..pos + 2].try_into().expect("2 bytes"));
let length = length_u16 as usize;
pos += 2;
if length == 0 {
return Err(corrupt(DeltaCorruption::ZeroLengthCopy));
}
let copy_past_base = || {
corrupt(DeltaCorruption::CopyPastBase {
offset: offset_u32,
length: length_u16,
base_len: base.len(),
})
};
let end = offset.checked_add(length).ok_or_else(copy_past_base)?;
if end > base.len() {
return Err(copy_past_base());
}
if out.len().checked_add(length).is_none_or(|v| v > result_len) {
return Err(corrupt(DeltaCorruption::ResultLenOverrun {
emitted: out.len(),
requested: length,
result_len,
}));
}
out.extend_from_slice(&base[offset..end]);
} else if op > 0 {
let length = op as usize;
if pos + length > stream.len() {
return Err(MkitError::UnexpectedEof);
}
if out.len().checked_add(length).is_none_or(|v| v > result_len) {
return Err(corrupt(DeltaCorruption::ResultLenOverrun {
emitted: out.len(),
requested: length,
result_len,
}));
}
out.extend_from_slice(&stream[pos..pos + length]);
pos += length;
} else {
return Err(corrupt(DeltaCorruption::ZeroOpcode));
}
}
if out.len() != result_len {
return Err(corrupt(DeltaCorruption::ResultLenUnderrun {
expected: result_len,
actual: out.len(),
}));
}
Ok(out)
}
fn write_header(out: &mut Vec<u8>, base_len: usize, result_len: usize) {
let bl: u32 = u32::try_from(base_len).expect("base_len <= u32::MAX (checked)");
let rl: u32 = u32::try_from(result_len).expect("result_len <= u32::MAX (checked)");
out.push(STREAM_VERSION);
out.extend_from_slice(&bl.to_le_bytes());
out.extend_from_slice(&rl.to_le_bytes());
}
fn emit_copy(out: &mut Vec<u8>, offset: u32, length: u16) {
out.push(OP_COPY);
out.extend_from_slice(&offset.to_le_bytes());
out.extend_from_slice(&length.to_le_bytes());
}
fn flush_insert(out: &mut Vec<u8>, buf: &mut Vec<u8>) {
if buf.is_empty() {
return;
}
debug_assert!(buf.len() <= MAX_INSERT_LEN);
out.push(u8::try_from(buf.len()).expect("<= 127"));
out.extend_from_slice(buf);
buf.clear();
}
const ROLL_M: u64 = 0x9E37_79B9_7F4A_7C15;
const ROLL_M_POW_BLOCK: u64 = {
let mut r: u64 = 1;
let mut i = 0;
while i < BLOCK_SIZE {
r = r.wrapping_mul(ROLL_M);
i += 1;
}
r
};
fn block_hash(block: &[u8]) -> u64 {
let mut h: u64 = 0;
for &b in block {
h = h.wrapping_mul(ROLL_M).wrapping_add(u64::from(b));
}
h
}
fn roll_forward(h: u64, old: u8, new: u8) -> u64 {
h.wrapping_mul(ROLL_M)
.wrapping_sub(u64::from(old).wrapping_mul(ROLL_M_POW_BLOCK))
.wrapping_add(u64::from(new))
}
#[cfg(test)]
mod tests {
use super::*;
fn header(base_len: u32, result_len: u32) -> [u8; HEADER_LEN] {
let mut h = [0u8; HEADER_LEN];
h[0] = STREAM_VERSION;
h[1..5].copy_from_slice(&base_len.to_le_bytes());
h[5..9].copy_from_slice(&result_len.to_le_bytes());
h
}
#[test]
fn random_seed_differs_across_calls_and_seeds_a_usable_map() {
use std::hash::BuildHasher;
fn hash_u64_with(state: &rustc_hash::FxSeededState, i: u64) -> u64 {
state.hash_one(i)
}
let seed_a = random_seed();
let seed_b = random_seed();
assert_ne!(
seed_a, seed_b,
"random_seed() returned the same value twice in a row"
);
let a = rustc_hash::FxSeededState::with_seed(seed_a);
let b = rustc_hash::FxSeededState::with_seed(seed_b);
assert_ne!(
hash_u64_with(&a, 42),
hash_u64_with(&b, 42),
"two different seeds produced the same hash for the same key"
);
assert_eq!(hash_u64_with(&a, 42), hash_u64_with(&a, 42));
}
#[test]
fn identity_roundtrip() {
let data = b"0123456789abcdef".repeat(4); let stream = encode(&data, &data).unwrap();
let restored = decode(&data, &stream).unwrap();
assert_eq!(restored, data);
}
#[test]
fn roll_forward_matches_direct_block_hash() {
let data: Vec<u8> = (0..256u32)
.map(|i| i.wrapping_mul(2_654_435_761).to_le_bytes()[0])
.collect();
assert!(data.len() > BLOCK_SIZE * 4);
let mut h = block_hash(&data[0..BLOCK_SIZE]);
for start in 0..data.len() - BLOCK_SIZE - 1 {
let direct = block_hash(&data[start + 1..start + 1 + BLOCK_SIZE]);
h = roll_forward(h, data[start], data[start + BLOCK_SIZE]);
assert_eq!(
h, direct,
"rolled hash diverged from direct block_hash at start={start}"
);
}
}
#[test]
fn pure_insert_roundtrip() {
let base = b"aaa";
let target = b"zzz";
let stream = encode(base, target).unwrap();
assert_eq!(stream[HEADER_LEN] & 0x80, 0);
assert_eq!(stream[HEADER_LEN], 3);
let restored = decode(base, &stream).unwrap();
assert_eq!(restored, target);
}
#[test]
fn pure_copy_full_base() {
let base: Vec<u8> = (0..16u8).cycle().take(128).collect();
let target = &base[..64];
let mut stream = header(
u32::try_from(base.len()).unwrap(),
u32::try_from(target.len()).unwrap(),
)
.to_vec();
stream.push(OP_COPY);
stream.extend_from_slice(&0u32.to_le_bytes());
stream.extend_from_slice(&64u16.to_le_bytes());
assert_eq!(stream.len(), HEADER_LEN + 7);
let restored = decode(&base, &stream).unwrap();
assert_eq!(restored, target);
}
#[test]
fn near_duplicate_yields_smaller_delta() {
let v1 = include_str!("delta.rs"); let mut v2 = String::from(v1);
v2.push_str("\n// trailing edit\n");
let stream = encode(v1.as_bytes(), v2.as_bytes()).unwrap();
let restored = decode(v1.as_bytes(), &stream).unwrap();
assert_eq!(restored, v2.as_bytes());
assert!(stream.len() < v2.len(), "delta should be smaller than v2");
}
#[test]
fn rejects_zero_opcode() {
let mut stream = header(0, 0).to_vec();
stream.push(0x00);
let err = decode(&[], &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::ZeroOpcode)
));
}
#[test]
fn rejects_unknown_version() {
let mut bytes = header(0, 0);
bytes[0] = 0x02;
let err = decode(&[], &bytes).unwrap_err();
assert!(matches!(err, MkitError::UnsupportedObjectVersion));
}
#[test]
fn rejects_truncated_header() {
let bytes = [0x01u8, 0x00, 0x00];
let err = decode(&[], &bytes).unwrap_err();
assert!(matches!(err, MkitError::UnexpectedEof));
}
#[test]
fn rejects_truncated_copy() {
let mut stream = header(16, 16).to_vec();
stream.push(OP_COPY);
stream.extend_from_slice(&0u32.to_le_bytes()); let err = decode(&[0u8; 16], &stream).unwrap_err();
assert!(matches!(err, MkitError::UnexpectedEof));
}
#[test]
fn rejects_truncated_insert() {
let mut stream = header(0, 10).to_vec();
stream.push(10); stream.extend_from_slice(b"abc"); let err = decode(&[], &stream).unwrap_err();
assert!(matches!(err, MkitError::UnexpectedEof));
}
#[test]
fn rejects_copy_past_base_end() {
let base = b"short"; let mut stream = header(u32::try_from(base.len()).unwrap(), 100).to_vec();
stream.push(OP_COPY);
stream.extend_from_slice(&0u32.to_le_bytes());
stream.extend_from_slice(&100u16.to_le_bytes());
let err = decode(base, &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::CopyPastBase { .. })
));
}
#[test]
fn rejects_copy_with_zero_length() {
let base = [0u8; 16];
let mut stream = header(16, 16).to_vec();
stream.push(OP_COPY);
stream.extend_from_slice(&0u32.to_le_bytes());
stream.extend_from_slice(&0u16.to_le_bytes());
let err = decode(&base, &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::ZeroLengthCopy)
));
}
#[test]
fn rejects_base_len_mismatch() {
let stream = header(16, 0).to_vec();
let err = decode(&[0u8; 8], &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::BaseLenMismatch {
declared: 16,
actual: 8
})
));
}
#[test]
fn rejects_result_len_mismatch_at_end() {
let mut stream = header(0, 3).to_vec();
stream.push(5);
stream.extend_from_slice(b"hello");
let err = decode(&[], &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::ResultLenOverrun { .. })
));
}
#[test]
fn rejects_huge_result_len_without_preallocating() {
let stream = header(0, u32::MAX);
let err = decode(&[], &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::ResultLenUnderrun { .. })
));
}
#[test]
fn rejects_copy_with_reserved_low_bits() {
let base = [0u8; 16];
let mut stream = header(16, 4).to_vec();
stream.push(OP_COPY | 0x01); stream.extend_from_slice(&0u32.to_le_bytes());
stream.extend_from_slice(&4u16.to_le_bytes());
let err = decode(&base, &stream).unwrap_err();
assert!(matches!(
err,
MkitError::DeltaCorrupt(DeltaCorruption::ReservedOpcodeBits(0x81))
));
}
#[test]
fn empty_base_pure_insert() {
let target = b"all new content here!";
let stream = encode(b"", target).unwrap();
let restored = decode(b"", &stream).unwrap();
assert_eq!(restored, target);
}
#[test]
fn cap_hint_does_not_scale_with_base_len() {
let huge_base = 1usize << 30; let tiny_stream = 9usize; let declared_result = u32::MAX as usize;
let cap = super::compute_cap_hint(declared_result, huge_base, tiny_stream);
assert!(
cap <= tiny_stream.saturating_mul(CAP_MULTIPLIER),
"cap_hint {cap} must be bounded by stream.len() * CAP_MULTIPLIER, \
not by base.len()",
);
assert!(
cap < 1024 * 1024,
"cap_hint {cap} must stay well below 1 MiB for a 9-byte stream",
);
}
#[test]
fn delta_corruption_is_not_reported_as_trailing_data() {
let base_len_mismatch = header(16, 0).to_vec();
let mut reserved_bits = header(16, 4).to_vec();
reserved_bits.push(OP_COPY | 0x01);
reserved_bits.extend_from_slice(&0u32.to_le_bytes());
reserved_bits.extend_from_slice(&4u16.to_le_bytes());
let mut zero_opcode = header(0, 0).to_vec();
zero_opcode.push(0x00);
let mut zero_length_copy = header(16, 16).to_vec();
zero_length_copy.push(OP_COPY);
zero_length_copy.extend_from_slice(&0u32.to_le_bytes());
zero_length_copy.extend_from_slice(&0u16.to_le_bytes());
let mut copy_past_base = header(5, 100).to_vec();
copy_past_base.push(OP_COPY);
copy_past_base.extend_from_slice(&0u32.to_le_bytes());
copy_past_base.extend_from_slice(&100u16.to_le_bytes());
let mut copy_result_overrun = header(16, 4).to_vec();
copy_result_overrun.push(OP_COPY);
copy_result_overrun.extend_from_slice(&0u32.to_le_bytes());
copy_result_overrun.extend_from_slice(&8u16.to_le_bytes());
let mut result_len_underrun = header(0, 3).to_vec();
result_len_underrun.push(5);
result_len_underrun.extend_from_slice(b"hello");
let cases: &[(&str, Vec<u8>, &[u8])] = &[
("base_len_mismatch", base_len_mismatch, &[0u8; 8]),
("reserved_bits", reserved_bits, &[0u8; 16]),
("zero_opcode", zero_opcode, &[]),
("zero_length_copy", zero_length_copy, &[0u8; 16]),
("copy_past_base", copy_past_base, b"short"),
("copy_result_overrun", copy_result_overrun, &[0u8; 16]),
("result_len_underrun", result_len_underrun, &[]),
];
for (name, stream, base) in cases {
let err = decode(base, stream).expect_err(&format!("{name} must be rejected"));
assert!(
!matches!(err, MkitError::TrailingData),
"{name} must not be reported as TrailingData, got {err:?}"
);
}
let data = b"0123456789abcdef".repeat(4);
let stream = encode(&data, &data).unwrap();
assert_eq!(decode(&data, &stream).unwrap(), data);
}
#[test]
fn check_length_bounds_rejects_over_u32() {
let over = (u32::MAX as usize).saturating_add(1);
assert!(matches!(
check_length_bounds(over, 0),
Err(MkitError::DeltaLengthOverflow { .. })
));
assert!(matches!(
check_length_bounds(0, over),
Err(MkitError::DeltaLengthOverflow { .. })
));
assert!(check_length_bounds(u32::MAX as usize, u32::MAX as usize).is_ok());
assert!(check_length_bounds(1, 1).is_ok());
}
proptest::proptest! {
#[test]
fn proptest_encode_decode_roundtrip(
base in proptest::collection::vec(proptest::num::u8::ANY, 0..4096),
result in proptest::collection::vec(proptest::num::u8::ANY, 0..4096),
) {
let stream = encode(&base, &result).unwrap();
let restored = decode(&base, &stream).unwrap();
proptest::prop_assert_eq!(restored, result);
}
#[test]
fn proptest_encode_decode_roundtrip_near_duplicate(
base in proptest::collection::vec(proptest::num::u8::ANY, 1..4096),
edit_pos_permille in 0u64..1000,
patch in proptest::collection::vec(proptest::num::u8::ANY, 0..64),
) {
let mut result = base.clone();
let edit_pos = (base.len() as u64 * edit_pos_permille / 1000) as usize;
result.splice(edit_pos..edit_pos, patch);
let stream = encode(&base, &result).unwrap();
let restored = decode(&base, &stream).unwrap();
proptest::prop_assert_eq!(restored, result);
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
const MAX_STREAM: usize = 20;
const MAX_BASE: usize = 4;
const fn unwind_for(s: usize) -> usize {
(s - HEADER_LEN) / 2 + 2
}
const _: [(); 7] = [(); unwind_for(MAX_STREAM)];
const _: [(); 5] = [(); unwind_for(16)];
#[derive(Debug, PartialEq, Eq)]
enum Class {
Eof,
Version,
Corrupt,
}
fn class_of(e: &MkitError) -> Class {
match e {
MkitError::UnexpectedEof => Class::Eof,
MkitError::UnsupportedObjectVersion => Class::Version,
MkitError::DeltaCorrupt(_) => Class::Corrupt,
_ => panic!("decode returned an error kind outside SPEC-DELTA §2/§4"),
}
}
fn spec_apply(base: &[u8], s: &[u8]) -> Result<Vec<u8>, Class> {
if s.len() < 9 {
return Err(Class::Eof);
}
if s[0] != 0x01 {
return Err(Class::Version);
}
let le32 = |p: usize| u32::from_le_bytes([s[p], s[p + 1], s[p + 2], s[p + 3]]) as u64;
if base.len() as u64 != le32(1) {
return Err(Class::Corrupt);
}
let result_len = le32(5);
let mut out = Vec::new();
let mut pos = 9usize;
while pos < s.len() {
let op = s[pos];
pos += 1;
if op & 0x80 != 0 {
if op & 0x7F != 0 {
return Err(Class::Corrupt);
}
if pos + 6 > s.len() {
return Err(Class::Eof);
}
let offset = le32(pos);
let length = u64::from(u16::from_le_bytes([s[pos + 4], s[pos + 5]]));
pos += 6;
if length == 0
|| offset + length > base.len() as u64
|| out.len() as u64 + length > result_len
{
return Err(Class::Corrupt);
}
out.extend_from_slice(&base[offset as usize..(offset + length) as usize]);
} else if op > 0 {
let n = op as usize;
if pos + n > s.len() {
return Err(Class::Eof);
}
if out.len() as u64 + n as u64 > result_len {
return Err(Class::Corrupt);
}
out.extend_from_slice(&s[pos..pos + n]);
pos += n;
} else {
return Err(Class::Corrupt);
}
}
if out.len() as u64 != result_len {
return Err(Class::Corrupt);
}
Ok(out)
}
fn any_input<const B: usize, const S: usize>() -> ([u8; B], usize, [u8; S], usize) {
(
kani::any(),
kani::any_where(|&n| n <= B),
kani::any(),
kani::any_where(|&n| n <= S),
)
}
#[kani::proof]
#[kani::unwind(7)] fn delta_decode_no_panic() {
let (b, bl, s, sl) = any_input::<MAX_BASE, MAX_STREAM>();
let stream = &s[..sl];
let got = decode(&b[..bl], stream);
if let Ok(out) = &got {
let declared = u32::from_le_bytes([stream[5], stream[6], stream[7], stream[8]]);
assert_eq!(out.len(), declared as usize);
}
kani::cover!(got.is_ok() && sl > HEADER_LEN, "ok_nonempty");
kani::cover!(
got.is_ok() && stream.get(HEADER_LEN) == Some(&OP_COPY),
"ok_copy"
);
kani::cover!(
matches!(
got,
Err(MkitError::DeltaCorrupt(
DeltaCorruption::ResultLenOverrun { .. }
))
),
"overrun"
);
}
fn spec_at<const S: usize>() -> Option<u8> {
let b: [u8; 3] = kani::any();
let bl: usize = kani::any_where(|&n| n <= 3);
let s: [u8; S] = kani::any();
let base = &b[..bl];
let got = decode(base, &s);
match (&got, &spec_apply(base, &s)) {
(Ok(out), Ok(expected)) => {
assert_eq!(out, expected);
}
(Err(e), Err(c)) => {
assert_eq!(&class_of(e), c);
}
_ => panic!("decode and SPEC-DELTA §4 model disagree on accept/reject"),
}
got.ok().and_then(|_| s.get(HEADER_LEN).copied())
}
#[kani::proof]
#[kani::unwind(7)] fn delta_spec_header() {
let _ = spec_at::<8>();
let _ = spec_at::<9>();
}
#[kani::proof]
#[kani::unwind(7)]
fn delta_spec_insert() {
let _ = spec_at::<10>();
kani::cover!(spec_at::<11>() == Some(1), "ok_insert");
}
#[kani::proof]
#[kani::unwind(7)]
fn delta_spec_copy() {
kani::cover!(spec_at::<16>() == Some(OP_COPY), "ok_copy");
}
#[kani::proof]
#[kani::unwind(7)] #[kani::should_panic]
fn delta_decode_canary_wrong_length() {
let stream_buf: [u8; MAX_STREAM] = kani::any();
let stream_len: usize = kani::any_where(|&n| n <= MAX_STREAM);
let stream = &stream_buf[..stream_len];
if let Ok(out) = decode(&[], stream) {
let declared = u32::from_le_bytes([stream[5], stream[6], stream[7], stream[8]]);
assert_eq!(out.len(), declared as usize + 1);
}
}
fn fixed_seed() -> usize {
0
}
fn roundtrip_at<const B: usize, const R: usize>() {
let b: [u8; B] = kani::any();
let r: [u8; R] = kani::any();
let stream = encode(&b, &r).expect("small lengths fit u32");
assert_eq!(decode(&b, &stream).expect("own stream decodes"), &r);
}
#[kani::proof]
#[kani::stub(random_seed, fixed_seed)]
#[kani::unwind(5)]
fn delta_encode_decode_roundtrip() {
macro_rules! each_r {
($b:literal) => {
roundtrip_at::<$b, 0>();
roundtrip_at::<$b, 1>();
roundtrip_at::<$b, 2>();
roundtrip_at::<$b, 3>();
};
}
each_r!(0);
each_r!(1);
each_r!(2);
each_r!(3);
}
}