use crate::index::{build_hash_map, RollingHash, HASH_BASE, WINDOW_SIZE};
use crate::memcmp::simd_memcmp;
use memchr::memmem;
use rustc_hash::FxHashMap;
const COPY_WIRE_LEN: usize = 9;
const MIN_MATCH_COMPRESSED: usize = 16;
const COMPRESSION_MATTERS_ABOVE: usize = 4096;
const MAX_GAPS: usize = 8;
const SHIFT_SLOTS: usize = 16;
const WINDOW_SHARE: usize = 128;
const WINDOW_MIN: usize = 4096;
const WINDOW_MAX: usize = 1 << 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Params {
pub min_match: usize,
pub anchor: usize,
pub anchor_verify: usize,
pub gaps: [usize; MAX_GAPS],
pub gap_count: usize,
pub window_half_width: usize,
pub slots: usize,
}
#[cfg(any(test, feature = "demo"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamsError {
GapLadder,
GapOrder,
MinMatchAboveAnchor,
VerifyBelowAnchor,
WindowBelowAnchor,
TooManySlots,
}
impl Params {
#[must_use]
pub fn for_input(new_len: usize) -> Self {
Self {
min_match: if new_len < COMPRESSION_MATTERS_ABOVE {
COPY_WIRE_LEN.saturating_add(1)
} else {
MIN_MATCH_COMPRESSED
},
anchor: 32,
anchor_verify: 64,
gaps: [0, 32, 128, 512, 2048, 0, 0, 0],
gap_count: 5,
window_half_width: (new_len / WINDOW_SHARE).clamp(WINDOW_MIN, WINDOW_MAX),
slots: SHIFT_SLOTS,
}
}
#[must_use]
pub fn gaps(&self) -> &[usize] {
self.gaps.get(..self.gap_count.min(MAX_GAPS)).unwrap_or(&[])
}
fn ladder(self) -> impl Iterator<Item = usize> {
self.gaps.into_iter().take(self.gap_count.min(MAX_GAPS))
}
fn window_widths(self) -> impl Iterator<Item = usize> {
let narrow = WINDOW_MIN.min(self.window_half_width);
let passes = if narrow < self.window_half_width {
2
} else {
1
};
[narrow, self.window_half_width].into_iter().take(passes)
}
#[must_use]
pub fn skip_ahead(&self) -> usize {
self.gaps().last().copied().unwrap_or(0)
}
#[must_use]
pub fn search_round_cost(&self) -> usize {
self.window_half_width
.saturating_mul(2)
.saturating_mul(self.gaps().len())
}
#[cfg(any(test, feature = "demo"))]
pub fn check(&self) -> Result<(), ParamsError> {
let gaps = self.gaps();
if gaps.first() != Some(&0) {
return Err(ParamsError::GapLadder);
}
if gaps.windows(2).any(|w| w.first() >= w.last()) {
return Err(ParamsError::GapOrder);
}
if self.min_match > self.anchor {
return Err(ParamsError::MinMatchAboveAnchor);
}
if self.anchor_verify < self.anchor {
return Err(ParamsError::VerifyBelowAnchor);
}
if self.window_half_width < self.anchor {
return Err(ParamsError::WindowBelowAnchor);
}
if self.slots > SHIFT_SLOTS {
return Err(ParamsError::TooManySlots);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PatchStats {
pub copies: u32,
pub literals: u32,
pub resyncs: u32,
pub shift_cache_hits: u32,
pub window_searches: u32,
pub old_bytes_scanned: u64,
pub escalations: u32,
pub restarts: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chunk {
Copy {
offset: usize,
len: usize,
},
Add {
start: usize,
len: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct At {
pub new: usize,
pub old: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Via {
Cache,
Window,
}
#[cfg_attr(not(feature = "demo"), allow(dead_code))]
#[derive(Debug, Clone, Copy)]
pub enum Event<'a> {
Compared {
run: usize,
accepted: bool,
},
EmittedCopy {
offset: usize,
len: usize,
},
ResyncOpened,
GapTooLate {
gap: usize,
},
GapProbed {
gap: usize,
at_new: usize,
at_old: usize,
anchor: &'a [u8],
need: usize,
drifts: &'a [i64],
},
DriftTried {
drift: i64,
at: Option<usize>,
extends: usize,
accepted: bool,
},
WindowOpened {
lo: usize,
hi: usize,
},
CandidateTried {
at: usize,
extends: usize,
accepted: bool,
},
Aligned {
via: Via,
gap: usize,
at: usize,
drift: i64,
literal: usize,
resume: usize,
walked_back: usize,
},
EmittedLiteral {
start: usize,
len: usize,
resume: usize,
},
ResyncFailed,
LadderWeighed {
scanned: u64,
threshold: u64,
escalating: bool,
},
IndexBuilt {
entries: usize,
key_bytes: usize,
positions: &'a [usize],
},
IndexProbed {
step: usize,
at: usize,
extends: usize,
accepted: bool,
},
IndexExhausted,
Reclaimed {
bytes: usize,
},
SkippedAhead {
len: usize,
},
Restarted {
carried: usize,
},
Finished,
}
pub trait Trace {
const ENABLED: bool;
fn record(&mut self, at: At, stats: &PatchStats, event: Event<'_>);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoTrace;
impl Trace for NoTrace {
const ENABLED: bool = false;
#[inline(always)]
fn record(&mut self, _at: At, _stats: &PatchStats, _event: Event<'_>) {}
}
pub fn diff(old: &[u8], new: &[u8], chunks: &mut Vec<Chunk>) -> PatchStats {
diff_with(old, new, Params::for_input(new.len()), chunks)
}
pub fn diff_with(old: &[u8], new: &[u8], params: Params, chunks: &mut Vec<Chunk>) -> PatchStats {
diff_traced(old, new, params, NoTrace, chunks).0
}
pub fn diff_traced<T: Trace>(
old: &[u8],
new: &[u8],
params: Params,
trace: T,
chunks: &mut Vec<Chunk>,
) -> (PatchStats, T) {
let mut differ = Differ {
old,
new,
shifts: ShiftCache::new(),
index: None,
params,
cursor_new: 0,
cursor_old: 0,
fruitless: 0,
wide_exhausted: false,
trace,
stats: PatchStats::default(),
};
differ.run(chunks);
(differ.stats, differ.trace)
}
struct Differ<'a, T: Trace> {
old: &'a [u8],
new: &'a [u8],
shifts: ShiftCache,
index: Option<FxHashMap<u64, usize>>,
params: Params,
cursor_new: usize,
cursor_old: usize,
fruitless: u64,
wide_exhausted: bool,
trace: T,
stats: PatchStats,
}
enum Step {
Realign { literal: usize, aligned: usize },
Skip { literal: usize },
Finish,
}
struct Probe<'a> {
gap: usize,
at_new: usize,
at_old: usize,
anchor: &'a [u8],
need: usize,
}
impl<'a, T: Trace> Differ<'a, T> {
fn at(&self) -> At {
At {
new: self.cursor_new,
old: self.cursor_old,
}
}
fn run(&mut self, chunks: &mut Vec<Chunk>) {
self.cursor_new = 0;
self.cursor_old = 0;
while self.cursor_new < self.new.len() {
let run = self.extend(self.cursor_new, self.cursor_old);
let long_enough = run >= self.params.min_match;
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::Compared {
run,
accepted: long_enough,
},
);
}
if long_enough {
chunks.push(Chunk::Copy {
offset: self.cursor_old,
len: run,
});
self.stats.copies = self.stats.copies.saturating_add(1);
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::EmittedCopy {
offset: self.cursor_old,
len: run,
},
);
}
self.cursor_new = self.cursor_new.saturating_add(run);
self.cursor_old = self.cursor_old.saturating_add(run);
if self.cursor_new >= self.new.len() {
break;
}
}
self.stats.resyncs = self.stats.resyncs.saturating_add(1);
if T::ENABLED {
let at = self.at();
self.trace.record(at, &self.stats, Event::ResyncOpened);
}
let blind = self.index.is_none();
let outcome = self.step();
if blind && self.index.is_some() && self.stats.restarts == 0 {
let carried = if T::ENABLED { literal_bytes(chunks) } else { 0 };
self.stats.restarts = 1;
self.stats.copies = 0;
self.stats.literals = 0;
chunks.clear();
self.cursor_new = 0;
self.cursor_old = 0;
if T::ENABLED {
let at = self.at();
self.trace
.record(at, &self.stats, Event::Restarted { carried });
}
continue;
}
match outcome {
Step::Realign { literal, aligned } if literal > 0 || aligned != self.cursor_old => {
let aligned = if literal == 0 {
let back = self.reclaim(chunks, aligned);
self.cursor_new = self.cursor_new.saturating_sub(back);
aligned.saturating_sub(back)
} else {
aligned
};
self.carry(chunks, literal, aligned);
}
Step::Skip { literal } if literal > 0 => {
let resume = self.cursor_old;
self.carry(chunks, literal, resume);
}
Step::Realign { .. } | Step::Skip { .. } | Step::Finish => {
let rest = self.new.len().saturating_sub(self.cursor_new);
let resume = self.cursor_old;
self.carry(chunks, rest, resume);
if T::ENABLED {
let at = self.at();
self.trace.record(at, &self.stats, Event::Finished);
}
break;
}
}
}
}
fn carry(&mut self, chunks: &mut Vec<Chunk>, literal: usize, resume: usize) {
let start = self.cursor_new;
self.push_literal(chunks, start, literal);
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::EmittedLiteral {
start,
len: literal,
resume,
},
);
}
self.cursor_new = self.cursor_new.saturating_add(literal);
self.cursor_old = resume;
}
fn step(&mut self) -> Step {
if let Some((literal, aligned)) = self.resync() {
return Step::Realign { literal, aligned };
}
if T::ENABLED {
let at = self.at();
self.trace.record(at, &self.stats, Event::ResyncFailed);
}
let spent = self
.fruitless
.saturating_add(u64::try_from(self.params.search_round_cost()).unwrap_or(u64::MAX));
let index_cost = u64::try_from(self.old.len()).unwrap_or(u64::MAX);
let escalating = self.index.is_some() || spent >= index_cost;
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::LadderWeighed {
scanned: self.stats.old_bytes_scanned,
threshold: index_cost,
escalating,
},
);
}
if escalating {
return match self.escalate() {
Some((literal, aligned)) => Step::Realign { literal, aligned },
None => Step::Finish,
};
}
let skip = self.params.skip_ahead();
if self.new.len().saturating_sub(self.cursor_new) <= skip {
Step::Finish
} else {
if T::ENABLED {
let at = self.at();
self.trace
.record(at, &self.stats, Event::SkippedAhead { len: skip });
}
Step::Skip { literal: skip }
}
}
fn extend(&self, at_new: usize, at_old: usize) -> usize {
match (self.new.get(at_new..), self.old.get(at_old..)) {
(Some(head), Some(tail)) => simd_memcmp(head, tail),
_ => 0,
}
}
fn push_literal(&mut self, chunks: &mut Vec<Chunk>, start: usize, len: usize) {
if len == 0 {
return;
}
if let Some(Chunk::Add {
start: prev_start,
len: prev_len,
}) = chunks.last_mut()
{
if prev_start.saturating_add(*prev_len) == start {
*prev_len = prev_len.saturating_add(len);
return;
}
}
chunks.push(Chunk::Add { start, len });
self.stats.literals = self.stats.literals.saturating_add(1);
}
fn resync(&mut self) -> Option<(usize, usize)> {
for gap in self.params.ladder() {
let Some(probe) = self.probe(gap) else {
if T::ENABLED {
let at = self.at();
self.trace
.record(at, &self.stats, Event::GapTooLate { gap });
}
continue;
};
self.announce_gap(&probe);
if let Some((pos, drift)) = self.probe_remembered(&probe) {
self.stats.shift_cache_hits = self.stats.shift_cache_hits.saturating_add(1);
self.wide_exhausted = false;
return Some(self.accept(Via::Cache, &probe, pos, drift));
}
}
if self.index.is_some() {
return None;
}
for half_width in self.params.window_widths() {
if half_width > WINDOW_MIN && self.wide_exhausted {
break;
}
for gap in self.params.ladder() {
let Some(probe) = self.probe(gap) else {
continue;
};
if let Some(pos) = self.search_window(&probe, half_width) {
let drift = shift_between(probe.at_old, pos).unwrap_or(0);
self.shifts.record(drift);
self.wide_exhausted = false;
return Some(self.accept(Via::Window, &probe, pos, drift));
}
}
}
self.wide_exhausted = true;
None
}
fn announce_gap(&mut self, probe: &Probe<'a>) {
if !T::ENABLED {
return;
}
let mut drifts = [0_i64; SHIFT_SLOTS];
drifts.copy_from_slice(&self.shifts.slots);
let len = self.shifts.len.min(self.params.slots);
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::GapProbed {
gap: probe.gap,
at_new: probe.at_new,
at_old: probe.at_old,
anchor: probe.anchor,
need: probe.need,
drifts: drifts.get(..len).unwrap_or(&[]),
},
);
}
fn accept(&mut self, via: Via, probe: &Probe<'_>, pos: usize, drift: i64) -> (usize, usize) {
let (literal, resume) = self.align(probe.gap, pos);
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::Aligned {
via,
gap: probe.gap,
at: pos,
drift,
literal,
resume,
walked_back: probe.gap.saturating_sub(literal),
},
);
}
(literal, resume)
}
fn probe(&self, gap: usize) -> Option<Probe<'a>> {
let new = self.new;
let at_new = self.cursor_new.checked_add(gap)?;
let anchor = new.get(at_new..at_new.checked_add(self.params.anchor)?)?;
Some(Probe {
gap,
at_new,
at_old: self.cursor_old.saturating_add(gap).min(self.old.len()),
anchor,
need: self
.params
.anchor_verify
.min(new.len().saturating_sub(at_new)),
})
}
fn probe_remembered(&mut self, probe: &Probe<'_>) -> Option<(usize, i64)> {
if let Some(pos) = self.try_drift(probe, 0) {
return Some((pos, 0));
}
for slot in 0..self.shifts.len.min(self.params.slots) {
let drift = self.shifts.slots.get(slot).copied()?;
if let Some(pos) = self.try_drift(probe, drift) {
self.shifts.record(drift);
return Some((pos, drift));
}
}
None
}
fn try_drift(&mut self, probe: &Probe<'_>, drift: i64) -> Option<usize> {
let pos = shift_position(probe.at_old, drift);
let extends = pos.map_or(0, |p| self.reach(probe, p));
let accepted = pos.is_some() && extends >= probe.need;
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::DriftTried {
drift,
at: pos,
extends,
accepted,
},
);
}
accepted.then_some(pos).flatten()
}
fn reach(&self, probe: &Probe<'_>, pos: usize) -> usize {
match (self.new.get(probe.at_new..), self.old.get(pos..)) {
(Some(head), Some(tail)) => simd_memcmp(head, tail),
_ => 0,
}
}
fn search_window(&mut self, probe: &Probe<'_>, half_width: usize) -> Option<usize> {
let lo = probe.at_old.saturating_sub(half_width);
let hi = probe
.at_old
.saturating_add(half_width)
.saturating_add(self.params.anchor)
.min(self.old.len());
self.search_range(probe, lo, hi)
}
fn search_range(&mut self, probe: &Probe<'_>, lo: usize, hi: usize) -> Option<usize> {
let haystack = self.old.get(lo..hi)?;
self.stats.window_searches = self.stats.window_searches.saturating_add(1);
if T::ENABLED {
let at = self.at();
self.trace
.record(at, &self.stats, Event::WindowOpened { lo, hi });
}
let mut accepted = None;
for offset in memmem::find_iter(haystack, probe.anchor) {
let pos = lo.saturating_add(offset);
let extends = self.reach(probe, pos);
let ok = extends >= probe.need;
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::CandidateTried {
at: pos,
extends,
accepted: ok,
},
);
}
if ok {
accepted = Some(offset);
break;
}
}
let walked = accepted.map_or(haystack.len(), |offset| {
offset.saturating_add(self.params.anchor)
});
let walked = u64::try_from(walked).unwrap_or(u64::MAX);
self.stats.old_bytes_scanned = self.stats.old_bytes_scanned.saturating_add(walked);
match accepted {
Some(_) => self.fruitless = 0,
None => self.fruitless = self.fruitless.saturating_add(walked),
}
accepted.map(|offset| lo.saturating_add(offset))
}
fn reclaim(&mut self, chunks: &mut Vec<Chunk>, aligned: usize) -> usize {
let Some(&Chunk::Add { start, len }) = chunks.last() else {
return 0;
};
if start.saturating_add(len) != self.cursor_new {
return 0;
}
let mut back: usize = 0;
while back < len {
let step = back.saturating_add(1);
let (Some(from_new), Some(from_old)) = (
self.cursor_new
.checked_sub(step)
.and_then(|i| self.new.get(i)),
aligned.checked_sub(step).and_then(|i| self.old.get(i)),
) else {
break;
};
if from_new != from_old {
break;
}
back = step;
}
if back == 0 {
return 0;
}
if back == len {
chunks.pop();
self.stats.literals = self.stats.literals.saturating_sub(1);
} else if let Some(Chunk::Add { len: carried, .. }) = chunks.last_mut() {
*carried = carried.saturating_sub(back);
}
if T::ENABLED {
let at = self.at();
self.trace
.record(at, &self.stats, Event::Reclaimed { bytes: back });
}
back
}
fn align(&self, gap: usize, pos: usize) -> (usize, usize) {
let mut literal = gap;
let mut aligned = pos;
while literal > 0 && aligned > 0 {
let from_new = self
.new
.get(self.cursor_new.saturating_add(literal).saturating_sub(1));
let from_old = self.old.get(aligned.saturating_sub(1));
if from_new.is_none() || from_new != from_old {
break;
}
literal = literal.saturating_sub(1);
aligned = aligned.saturating_sub(1);
}
(literal, aligned)
}
fn escalate(&mut self) -> Option<(usize, usize)> {
self.stats.escalations = self.stats.escalations.saturating_add(1);
if self.index.is_none() {
let built = build_hash_map(self.old);
if T::ENABLED {
let mut positions: Vec<usize> = built.values().copied().collect();
positions.sort_unstable();
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::IndexBuilt {
entries: built.len(),
key_bytes: WINDOW_SIZE,
positions: &positions,
},
);
}
self.index = Some(built);
}
let cursor = self.cursor_new;
let tail = self.new.get(cursor..)?;
let hashes = RollingHash::new(tail, WINDOW_SIZE, HASH_BASE)?.enumerate();
for (step, hash) in hashes {
let Some(&pos) = self.index.as_ref()?.get(&hash) else {
continue;
};
let extends = match (
self.new.get(cursor.saturating_add(step)..),
self.old.get(pos..),
) {
(Some(head), Some(candidate)) => simd_memcmp(head, candidate),
_ => 0,
};
let accepted = extends >= self.params.min_match;
if T::ENABLED {
let at = self.at();
self.trace.record(
at,
&self.stats,
Event::IndexProbed {
step,
at: pos,
extends,
accepted,
},
);
}
if accepted {
return Some((step, pos));
}
}
if T::ENABLED {
let at = self.at();
self.trace.record(at, &self.stats, Event::IndexExhausted);
}
None
}
}
fn literal_bytes(chunks: &[Chunk]) -> usize {
chunks
.iter()
.map(|chunk| match *chunk {
Chunk::Add { len, .. } => len,
Chunk::Copy { .. } => 0,
})
.sum()
}
struct ShiftCache {
slots: [i64; SHIFT_SLOTS],
len: usize,
}
impl ShiftCache {
const fn new() -> Self {
Self {
slots: [0; SHIFT_SLOTS],
len: 0,
}
}
fn record(&mut self, shift: i64) {
if shift == 0 {
return;
}
let known = self.slots.iter().take(self.len).position(|&s| s == shift);
let mut at = known.unwrap_or_else(|| self.len.min(SHIFT_SLOTS.saturating_sub(1)));
while at > 0 {
let previous = self.slots.get(at.saturating_sub(1)).copied().unwrap_or(0);
if let Some(slot) = self.slots.get_mut(at) {
*slot = previous;
}
at = at.saturating_sub(1);
}
if let Some(slot) = self.slots.first_mut() {
*slot = shift;
}
if known.is_none() {
self.len = self.len.saturating_add(1).min(SHIFT_SLOTS);
}
}
}
fn shift_between(from: usize, to: usize) -> Option<i64> {
i64::try_from(to)
.ok()?
.checked_sub(i64::try_from(from).ok()?)
}
fn shift_position(base: usize, shift: i64) -> Option<usize> {
usize::try_from(i64::try_from(base).ok()?.checked_add(shift)?).ok()
}