use std::num::NonZeroU64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct DiffRun {
start: u64,
len: NonZeroU64,
}
impl DiffRun {
const fn single(start: u64) -> Self {
Self {
start,
len: NonZeroU64::MIN,
}
}
pub(super) const fn start(self) -> u64 {
self.start
}
pub(super) const fn len(self) -> NonZeroU64 {
self.len
}
pub(super) const fn end(self) -> u64 {
self.start + self.len.get()
}
fn extend_to(&mut self, offset: u64) {
let Some(covered) = offset
.checked_sub(self.start)
.and_then(|distance| distance.checked_add(1))
.and_then(NonZeroU64::new)
else {
return;
};
self.len = self.len.max(covered);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::inspect) struct DiffSummary {
len_a: u64,
len_b: u64,
differing: u64,
runs: Vec<DiffRun>,
}
impl DiffSummary {
pub(super) fn first(&self) -> Option<u64> {
self.runs.first().map(|run| run.start)
}
pub(super) const fn len_a(&self) -> u64 {
self.len_a
}
pub(super) const fn len_b(&self) -> u64 {
self.len_b
}
pub(super) fn compared(&self) -> u64 {
self.len_a.min(self.len_b)
}
pub(super) const fn differing(&self) -> u64 {
self.differing
}
pub(super) fn runs(&self) -> &[DiffRun] {
&self.runs
}
pub(super) const fn identical(&self) -> bool {
self.len_a == self.len_b && self.differing == 0
}
}
pub(super) fn compare(a: &[u8], b: &[u8], gap: u64) -> DiffSummary {
let compared = a.len().min(b.len());
let mut runs: Vec<DiffRun> = Vec::new();
let mut differing = 0u64;
for offset in 0..compared {
if a[offset] == b[offset] {
continue;
}
differing += 1;
let offset = cadmpeg_core::decode::u64_from_index(offset);
match runs.last_mut() {
Some(last) => match last.end().checked_add(gap) {
Some(end) if offset <= end => last.extend_to(offset),
None => last.extend_to(offset),
Some(_) => runs.push(DiffRun::single(offset)),
},
None => runs.push(DiffRun::single(offset)),
}
}
DiffSummary {
len_a: cadmpeg_core::decode::u64_from_index(a.len()),
len_b: cadmpeg_core::decode::u64_from_index(b.len()),
differing,
runs,
}
}
#[cfg(test)]
mod tests {
use super::{compare, DiffRun};
fn run(start: u64, len: u64) -> DiffRun {
let mut run = DiffRun::single(start);
run.extend_to(start + len - 1);
run
}
#[test]
fn unrepresentable_diff_span_does_not_extend_a_run() {
let mut run = DiffRun::single(0);
run.extend_to(u64::MAX);
assert_eq!(run.len().get(), 1);
}
#[test]
fn identical_inputs_produce_no_runs() {
let summary = compare(b"abcd", b"abcd", 0);
assert!(summary.identical());
assert_eq!(summary.differing, 0);
assert_eq!(summary.first(), None);
assert!(summary.runs.is_empty());
}
#[test]
fn a_length_difference_alone_is_not_identical() {
let summary = compare(b"abcd", b"abcdef", 0);
assert!(!summary.identical());
assert_eq!(summary.compared(), 4);
assert_eq!(summary.differing, 0);
assert_eq!(summary.first(), None);
assert_eq!((summary.len_a, summary.len_b), (4, 6));
}
#[test]
fn reports_the_first_difference_and_each_byte_at_gap_zero() {
let summary = compare(b"abcde", b"aXcYe", 0);
assert_eq!(summary.first(), Some(1));
assert_eq!(summary.differing, 2);
assert_eq!(summary.runs, [DiffRun::single(1), DiffRun::single(3),]);
}
#[test]
fn coalesces_spans_separated_by_at_most_the_gap() {
let summary = compare(b"abcde", b"aXcYe", 1);
assert_eq!(summary.differing, 2);
assert_eq!(summary.runs, [run(1, 3)]);
let wide = compare(b"abcde", b"XbcdY", 1);
assert_eq!(wide.runs, [DiffRun::single(0), DiffRun::single(4),]);
let merged = compare(b"abcde", b"XbcdY", 3);
assert_eq!(merged.runs, [run(0, 5)]);
}
#[test]
fn a_run_of_adjacent_differences_merges_at_any_gap() {
let summary = compare(&[0, 0, 0, 0], &[0, 1, 2, 3], 0);
assert_eq!(summary.runs, [run(1, 3)]);
assert_eq!(summary.differing, 3);
}
#[test]
fn only_the_common_prefix_is_compared() {
let summary = compare(b"abc", b"abcZZZZ", 0);
assert_eq!(summary.compared(), 3);
assert_eq!(summary.differing, 0);
assert_eq!(summary.runs, []);
}
#[test]
fn empty_inputs_compare_cleanly() {
let summary = compare(&[], &[], 0);
assert!(summary.identical());
assert_eq!(summary.compared(), 0);
}
}