use std::sync::atomic::{AtomicBool, Ordering};
use super::ContentOptions;
use crate::content_diff::{self, BinaryMode, ContentDiff, ContentEdit, DiffError, DiffLimits};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum Whitespace {
#[default]
Exact,
IgnoreSpaceChange,
IgnoreAllSpace,
IgnoreSpaceAtEol,
IgnoreCrAtEol,
}
pub(super) fn compare(
base: &[u8],
side: &[u8],
options: ContentOptions,
cancel: &AtomicBool,
) -> Result<(Vec<ContentEdit>, bool), DiffError> {
if options.whitespace == Whitespace::Exact {
return Ok(unpack(content_diff::diff(
base,
side,
options.binary,
options.limits,
cancel,
)?));
}
check(cancel)?;
let bytes = base
.len()
.checked_add(side.len())
.ok_or(DiffError::Limit("input bytes"))?;
if bytes > options.limits.max_input_bytes {
return Err(DiffError::Limit("input bytes"));
}
let work = bytes
.checked_mul(4)
.ok_or(DiffError::Limit("whitespace work"))?;
let remaining = options
.limits
.max_work
.checked_sub(work)
.ok_or(DiffError::Limit("whitespace work"))?;
let binary = match options.binary {
BinaryMode::Auto => base.contains(&0) || side.contains(&0),
BinaryMode::Text => false,
BinaryMode::Binary => true,
};
if base == side {
return Ok((Vec::new(), false));
}
if binary {
return Ok((Vec::new(), true));
}
let a = keys(base, options.whitespace, options.limits, cancel)?;
let b = keys(side, options.whitespace, options.limits, cancel)?;
let mut limits = options.limits;
limits.max_work = remaining;
limits.max_input_bytes = limits
.max_input_bytes
.saturating_add(limits.max_lines.saturating_mul(2));
let (mut edits, _) = unpack(content_diff::diff(
&a.bytes,
&b.bytes,
BinaryMode::Text,
limits,
cancel,
)?);
for edit in &mut edits {
edit.old_bytes = a.offsets[edit.old_lines.start]..a.offsets[edit.old_lines.end];
edit.new_bytes = b.offsets[edit.new_lines.start]..b.offsets[edit.new_lines.end];
}
Ok((edits, false))
}
fn unpack(diff: ContentDiff<'_>) -> (Vec<ContentEdit>, bool) {
match diff {
ContentDiff::Text(text) => (text.edits().to_vec(), false),
ContentDiff::Binary { .. } => (Vec::new(), true),
ContentDiff::Unchanged => (Vec::new(), false),
}
}
pub(super) fn equivalent(
a: &[u8],
b: &[u8],
options: ContentOptions,
cancel: &AtomicBool,
) -> Result<bool, DiffError> {
if a == b {
return Ok(true);
}
if options.whitespace == Whitespace::Exact {
return Ok(false);
}
Ok(keys(a, options.whitespace, options.limits, cancel)?.bytes
== keys(b, options.whitespace, options.limits, cancel)?.bytes)
}
struct Keys {
bytes: Vec<u8>,
offsets: Vec<usize>,
}
fn keys(
input: &[u8],
policy: Whitespace,
limits: DiffLimits,
cancel: &AtomicBool,
) -> Result<Keys, DiffError> {
let mut bytes = Vec::new();
let mut offsets = vec![0];
let mut offset = 0;
for line in input.split_inclusive(|b| *b == b'\n') {
check(cancel)?;
if offsets.len() > limits.max_lines {
return Err(DiffError::Limit("lines"));
}
let body = line.strip_suffix(b"\n").unwrap_or(line);
line_key(body, policy, &mut bytes, cancel)?;
bytes.push(u8::from(line.ends_with(b"\n")));
bytes.push(b'\n');
offset += line.len();
offsets.push(offset);
}
Ok(Keys { bytes, offsets })
}
fn line_key(
body: &[u8],
policy: Whitespace,
output: &mut Vec<u8>,
cancel: &AtomicBool,
) -> Result<(), DiffError> {
match policy {
Whitespace::Exact => output.extend_from_slice(body),
Whitespace::IgnoreSpaceAtEol => {
let end = body
.iter()
.rposition(|b| !b.is_ascii_whitespace())
.map_or(0, |p| p + 1);
output.extend_from_slice(&body[..end]);
}
Whitespace::IgnoreCrAtEol => {
output.extend_from_slice(body.strip_suffix(b"\r").unwrap_or(body))
}
Whitespace::IgnoreSpaceChange | Whitespace::IgnoreAllSpace => {
let mut space = false;
for (position, byte) in body.iter().enumerate() {
if position % 65_536 == 0 {
check(cancel)?;
}
if byte.is_ascii_whitespace() {
space = true;
} else {
if space && policy == Whitespace::IgnoreSpaceChange {
output.push(b' ');
}
output.push(*byte);
space = false;
}
}
}
}
Ok(())
}
fn check(cancel: &AtomicBool) -> Result<(), DiffError> {
if cancel.load(Ordering::Relaxed) {
Err(DiffError::Cancelled)
} else {
Ok(())
}
}