mod blobs;
mod myers;
mod types;
use std::sync::atomic::{AtomicBool, Ordering};
pub use blobs::{BlobContent, ContentReadError};
pub use types::{BinaryMode, ContentDiff, ContentEdit, DiffError, DiffLimits, TextDiff};
pub fn diff<'a>(
old: &'a [u8],
new: &'a [u8],
mode: BinaryMode,
limits: DiffLimits,
cancel: &AtomicBool,
) -> Result<ContentDiff<'a>, DiffError> {
let mut budget = Budget {
remaining: limits.max_work,
cancel,
};
budget.check()?;
let bytes = old
.len()
.checked_add(new.len())
.ok_or(DiffError::Limit("input bytes"))?;
if bytes > limits.max_input_bytes {
return Err(DiffError::Limit("input bytes"));
}
if equal(old, new, &mut budget)? {
budget.check()?;
return Ok(ContentDiff::Unchanged);
}
let binary = match mode {
BinaryMode::Auto => has_nul(old, &mut budget)? || has_nul(new, &mut budget)?,
BinaryMode::Text => false,
BinaryMode::Binary => true,
};
if binary {
budget.check()?;
return Ok(ContentDiff::Binary { old, new });
}
let mut remaining_lines = limits.max_lines;
let old_lines = Lines::new(old, &mut remaining_lines, &mut budget)?;
let new_lines = Lines::new(new, &mut remaining_lines, &mut budget)?;
let edits = myers::edits(&old_lines, &new_lines, limits.max_trace, &mut budget)?;
budget.check()?;
Ok(ContentDiff::Text(TextDiff { old, new, edits }))
}
fn equal(old: &[u8], new: &[u8], budget: &mut Budget<'_>) -> Result<bool, DiffError> {
if old.len() != new.len() {
return Ok(false);
}
for (a, b) in old.chunks(4096).zip(new.chunks(4096)) {
budget.work(a.len())?;
if a != b {
return Ok(false);
}
}
Ok(true)
}
fn has_nul(bytes: &[u8], budget: &mut Budget<'_>) -> Result<bool, DiffError> {
for chunk in bytes.chunks(4096) {
budget.work(chunk.len())?;
if chunk.contains(&0) {
return Ok(true);
}
}
Ok(false)
}
struct Lines<'a> {
bytes: &'a [u8],
ends: Vec<usize>,
}
impl<'a> Lines<'a> {
fn new(
bytes: &'a [u8],
remaining: &mut usize,
budget: &mut Budget<'_>,
) -> Result<Self, DiffError> {
let mut ends = Vec::new();
for (chunk_index, chunk) in bytes.chunks(4096).enumerate() {
budget.work(chunk.len())?;
for (offset, byte) in chunk.iter().enumerate() {
if *byte == b'\n' {
charge(remaining, 1, "lines")?;
ends.push(chunk_index * 4096 + offset + 1);
}
}
}
if !bytes.is_empty() && bytes.last() != Some(&b'\n') {
charge(remaining, 1, "lines")?;
ends.push(bytes.len());
}
Ok(Self { bytes, ends })
}
fn len(&self) -> usize {
self.ends.len()
}
fn offset(&self, line: usize) -> usize {
if line == 0 { 0 } else { self.ends[line - 1] }
}
fn line(&self, index: usize) -> &[u8] {
&self.bytes[self.offset(index)..self.ends[index]]
}
fn range(&self, lines: &std::ops::Range<usize>) -> std::ops::Range<usize> {
self.offset(lines.start)..self.offset(lines.end)
}
}
struct Budget<'a> {
remaining: usize,
cancel: &'a AtomicBool,
}
impl Budget<'_> {
fn check(&self) -> Result<(), DiffError> {
if self.cancel.load(Ordering::Relaxed) {
Err(DiffError::Cancelled)
} else {
Ok(())
}
}
fn work(&mut self, count: usize) -> Result<(), DiffError> {
self.check()?;
charge(&mut self.remaining, count, "work")
}
}
fn charge(remaining: &mut usize, count: usize, name: &'static str) -> Result<(), DiffError> {
*remaining = remaining.checked_sub(count).ok_or(DiffError::Limit(name))?;
Ok(())
}
#[cfg(test)]
mod tests;