use std::io::{BufReader, Read};
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct ComparisonOptions {
pub skip_1: Option<usize>,
pub skip_2: Option<usize>,
pub max_bytes: Option<usize>,
pub marker: Option<Vec<u8>>,
pub percentage_limit: Option<f64>,
pub absolute_limit: Option<usize>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ComparisonDetails {
pub size: usize,
pub marker_1: Vec<u8>,
pub marker_2: Vec<u8>,
pub first_difference_offset: usize,
pub first_difference_line: usize,
pub first_difference_byte_1: Option<u8>,
pub first_difference_byte_2: Option<u8>,
pub differences: Vec<(usize, Option<u8>, Option<u8>)>,
}
impl Default for ComparisonDetails {
fn default() -> Self {
Self::new()
}
}
impl ComparisonDetails {
fn new() -> Self {
Self {
size: 0,
marker_1: vec![],
marker_2: vec![],
first_difference_offset: 0,
first_difference_line: 1,
first_difference_byte_1: None,
first_difference_byte_2: None,
differences: vec![],
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ComparisonResult {
Identical,
SimilarPercentage(f64, f64),
SimilarAbsolute(usize, usize),
Different(ComparisonDetails),
PercentageLimitExceeded(f64, f64),
AbsoluteLimitExceeded(usize, usize),
InvalidMarker1(Vec<u8>, Vec<u8>),
InvalidMarker2(Vec<u8>, Vec<u8>),
Error(String),
}
pub fn compare(reader_1: impl Read, reader_2: impl Read, options: &ComparisonOptions) -> ComparisonResult {
let mut details = ComparisonDetails::default();
let buf_1 = BufReader::new(reader_1);
let buf_2 = BufReader::new(reader_2);
let mut iter_1 = buf_1
.bytes()
.skip(options.skip_1.unwrap_or(0))
.take(options.max_bytes.unwrap_or(usize::MAX));
let mut iter_2 = buf_2
.bytes()
.skip(options.skip_2.unwrap_or(0))
.take(options.max_bytes.unwrap_or(usize::MAX));
let mut first_difference = false;
loop {
match (iter_1.next(), iter_2.next()) {
(Some(Ok(byte_1)), Some(Ok(byte_2))) => {
if !first_difference && byte_1 == b'\n' {
details.first_difference_line += 1;
}
if let Some(marker) = &options.marker {
if details.size < marker.len() {
details.marker_1.push(byte_1);
details.marker_2.push(byte_2);
}
}
details.size += 1;
if byte_1 != byte_2 {
if !first_difference {
details.first_difference_offset = details.size;
details.first_difference_byte_1 = Some(byte_1);
details.first_difference_byte_2 = Some(byte_2);
}
details.differences.push((details.size, Some(byte_1), Some(byte_2)));
first_difference = true;
}
}
(None, Some(Ok(byte_2))) => {
details.size += 1;
if !first_difference {
details.first_difference_offset = details.size;
details.first_difference_byte_2 = Some(byte_2);
}
details.differences.push((details.size, None, Some(byte_2)));
first_difference = true;
}
(Some(Ok(byte_1)), None) => {
if !first_difference && byte_1 == b'\n' {
details.first_difference_line += 1;
}
details.size += 1;
if !first_difference {
details.first_difference_offset = details.size;
details.first_difference_byte_1 = Some(byte_1);
}
details.differences.push((details.size, Some(byte_1), None));
first_difference = true;
}
(None, None) => break,
(value_1, value_2) => {
return ComparisonResult::Error(format!("Reading bytes failed. {:?} {:?}", value_1, value_2))
}
}
}
if let Some(marker) = options.marker.clone() {
if marker != details.marker_1 {
return ComparisonResult::InvalidMarker1(marker, details.marker_1.clone());
}
if marker != details.marker_2 {
return ComparisonResult::InvalidMarker2(marker, details.marker_2.clone());
}
}
let absolute_difference = details.differences.len();
if let Some(limit) = options.percentage_limit {
let percentage_difference = absolute_difference as f64 * 100.0 / details.size as f64;
return if percentage_difference > limit {
ComparisonResult::PercentageLimitExceeded(limit, percentage_difference)
} else {
ComparisonResult::SimilarPercentage(limit, percentage_difference)
};
}
if let Some(limit) = options.absolute_limit {
return if absolute_difference > limit {
ComparisonResult::AbsoluteLimitExceeded(limit, absolute_difference)
} else {
ComparisonResult::SimilarAbsolute(limit, absolute_difference)
};
}
if absolute_difference > 0 {
return ComparisonResult::Different(details);
}
ComparisonResult::Identical
}