semdiff-differ-binary 0.4.2

Binary diff calculator and reporters for semdiff.
Documentation
use crate::{BinaryDiff, BinaryDiffReporter};
use askama::Template;
use semdiff_core::fs::FileLeaf;
use semdiff_core::{DetailReporter, MayUnsupported};
use semdiff_output::html::{HtmlReport, HtmlReportError};
use similar::ChangeTag;
use similar::utils::TextDiffRemapper;
use std::fmt;
use std::fmt::Display;
use thiserror::Error;

const COMPARES_NAME: &str = "binary";

#[derive(Debug, Error)]
pub enum BinaryDiffReportError {
    #[error("html report error: {0}")]
    HtmlReport(#[from] HtmlReportError),
}

#[derive(Template)]
#[template(path = "binary_preview.html")]
struct BinaryPreviewTemplate {
    body: BinaryPreviewBody,
}

enum BinaryPreviewBody {
    Modified {
        expected_size: usize,
        actual_size: usize,
        added_bytes: usize,
        deleted_bytes: usize,
    },
    Single {
        size: usize,
    },
}

#[derive(Template)]
#[template(path = "binary_detail.html")]
struct BinaryDetailTemplate<'a> {
    detail: BinaryDetailBody<'a>,
}

enum BinaryDetailBody<'a> {
    Diff {
        expected: &'a [u8],
        actual: &'a [u8],
        diff: &'a similar::TextDiff<'a, 'a, [u8]>,
    },
    Single {
        label: &'a str,
        body: &'a [u8],
    },
}

fn diff_iter<'a>(
    diff: &'a similar::TextDiff<'a, 'a, [u8]>,
    expected: &'a [u8],
    actual: &'a [u8],
) -> impl Iterator<Item = (ChangeTag, &'a [u8])> {
    let remapper = TextDiffRemapper::from_text_diff(diff, expected, actual);
    diff.ops().iter().flat_map(move |x| remapper.iter_slices(x))
}

fn format_line(line: &[u8]) -> impl Display + '_ {
    fmt::from_fn(|f| {
        let Some((first, tail)) = line.split_first() else {
            return Ok(());
        };
        write!(f, "{:02X}", first)?;
        for byte in tail {
            write!(f, " {:02X}", byte)?;
        }
        Ok(())
    })
}

struct IncrementUsize {
    value: usize,
}

impl IncrementUsize {
    fn new() -> IncrementUsize {
        IncrementUsize { value: 0 }
    }

    fn incr(&mut self, value: usize) -> usize {
        let old = self.value;
        self.value += value;
        old
    }
}

impl BinaryDetailBody<'_> {
    fn is_multicolumn(&self) -> bool {
        matches!(self, BinaryDetailBody::Diff { .. })
    }
}

impl DetailReporter<BinaryDiff, FileLeaf, HtmlReport> for BinaryDiffReporter {
    type Error = BinaryDiffReportError;

    fn report_unchanged(
        &self,
        name: &str,
        diff: &BinaryDiff,
        reporter: &HtmlReport,
    ) -> Result<MayUnsupported<()>, Self::Error> {
        let preview_html = BinaryPreviewTemplate {
            body: BinaryPreviewBody::Single {
                size: diff.expected().len(),
            },
        };
        let detail_html = BinaryDetailTemplate {
            detail: BinaryDetailBody::Single {
                label: "same",
                body: diff.expected(),
            },
        };
        reporter.record_unchanged(name, COMPARES_NAME, preview_html, detail_html)?;
        Ok(MayUnsupported::Ok(()))
    }

    fn report_modified(
        &self,
        name: &str,
        diff: &BinaryDiff,
        reporter: &HtmlReport,
    ) -> Result<MayUnsupported<()>, Self::Error> {
        let diff_changes = diff.changes();
        let stat = BinaryDiff::stat(&diff_changes);
        let preview_html = BinaryPreviewTemplate {
            body: BinaryPreviewBody::Modified {
                expected_size: diff.expected().len(),
                actual_size: diff.actual().len(),
                added_bytes: stat.added,
                deleted_bytes: stat.deleted,
            },
        };
        let detail_html = BinaryDetailTemplate {
            detail: BinaryDetailBody::Diff {
                expected: diff.expected(),
                actual: diff.actual(),
                diff: &diff_changes,
            },
        };
        reporter.record_modified(name, COMPARES_NAME, preview_html, detail_html)?;
        Ok(MayUnsupported::Ok(()))
    }

    fn report_added(
        &self,
        name: &str,
        data: &FileLeaf,
        reporter: &HtmlReport,
    ) -> Result<MayUnsupported<()>, Self::Error> {
        let preview_html = BinaryPreviewTemplate {
            body: BinaryPreviewBody::Single {
                size: data.content.len(),
            },
        };
        let detail_html = BinaryDetailTemplate {
            detail: BinaryDetailBody::Single {
                label: "added",
                body: &data.content,
            },
        };
        reporter.record_added(name, COMPARES_NAME, preview_html, detail_html)?;
        Ok(MayUnsupported::Ok(()))
    }

    fn report_deleted(
        &self,
        name: &str,
        data: &FileLeaf,
        reporter: &HtmlReport,
    ) -> Result<MayUnsupported<()>, Self::Error> {
        let preview_html = BinaryPreviewTemplate {
            body: BinaryPreviewBody::Single {
                size: data.content.len(),
            },
        };
        let detail_html = BinaryDetailTemplate {
            detail: BinaryDetailBody::Single {
                label: "deleted",
                body: &data.content,
            },
        };
        reporter.record_deleted(name, COMPARES_NAME, preview_html, detail_html)?;
        Ok(MayUnsupported::Ok(()))
    }
}