Skip to main content

semdiff_differ_binary/
report_html.rs

1use crate::{BinaryDiff, BinaryDiffReporter};
2use askama::Template;
3use semdiff_core::fs::FileLeaf;
4use semdiff_core::{DetailReporter, MayUnsupported};
5use semdiff_output::html::{HtmlReport, HtmlReportError};
6use similar::ChangeTag;
7use similar::utils::TextDiffRemapper;
8use std::fmt;
9use std::fmt::Display;
10use thiserror::Error;
11
12const COMPARES_NAME: &str = "binary";
13
14#[derive(Debug, Error)]
15pub enum BinaryDiffReportError {
16    #[error("html report error: {0}")]
17    HtmlReport(#[from] HtmlReportError),
18}
19
20#[derive(Template)]
21#[template(path = "binary_preview.html")]
22struct BinaryPreviewTemplate {
23    body: BinaryPreviewBody,
24}
25
26enum BinaryPreviewBody {
27    Modified {
28        expected_size: usize,
29        actual_size: usize,
30        added_bytes: usize,
31        deleted_bytes: usize,
32    },
33    Single {
34        size: usize,
35    },
36}
37
38#[derive(Template)]
39#[template(path = "binary_detail.html")]
40struct BinaryDetailTemplate<'a> {
41    detail: BinaryDetailBody<'a>,
42}
43
44enum BinaryDetailBody<'a> {
45    Diff {
46        expected: &'a [u8],
47        actual: &'a [u8],
48        diff: &'a similar::TextDiff<'a, 'a, 'a, [u8]>,
49    },
50    Single {
51        label: &'a str,
52        body: &'a [u8],
53    },
54}
55
56fn diff_iter<'a>(
57    diff: &'a similar::TextDiff<[u8]>,
58    expected: &'a [u8],
59    actual: &'a [u8],
60) -> impl Iterator<Item = (ChangeTag, &'a [u8])> {
61    let remapper = TextDiffRemapper::from_text_diff(diff, expected, actual);
62    diff.ops().iter().flat_map(move |x| remapper.iter_slices(x))
63}
64
65fn format_line(line: &[u8]) -> impl Display + '_ {
66    fmt::from_fn(|f| {
67        let Some((first, tail)) = line.split_first() else {
68            return Ok(());
69        };
70        write!(f, "{:02X}", first)?;
71        for byte in tail {
72            write!(f, " {:02X}", byte)?;
73        }
74        Ok(())
75    })
76}
77
78struct IncrementUsize {
79    value: usize,
80}
81
82impl IncrementUsize {
83    fn new() -> IncrementUsize {
84        IncrementUsize { value: 0 }
85    }
86
87    fn incr(&mut self, value: usize) -> usize {
88        let old = self.value;
89        self.value += value;
90        old
91    }
92}
93
94impl BinaryDetailBody<'_> {
95    fn is_multicolumn(&self) -> bool {
96        matches!(self, BinaryDetailBody::Diff { .. })
97    }
98}
99
100impl DetailReporter<BinaryDiff, FileLeaf, HtmlReport> for BinaryDiffReporter {
101    type Error = BinaryDiffReportError;
102
103    fn report_unchanged(
104        &self,
105        name: &str,
106        diff: BinaryDiff,
107        reporter: &HtmlReport,
108    ) -> Result<MayUnsupported<()>, Self::Error> {
109        let preview_html = BinaryPreviewTemplate {
110            body: BinaryPreviewBody::Single {
111                size: diff.expected().len(),
112            },
113        };
114        let detail_html = BinaryDetailTemplate {
115            detail: BinaryDetailBody::Single {
116                label: "same",
117                body: diff.expected(),
118            },
119        };
120        reporter.record_unchanged(name, COMPARES_NAME, preview_html, detail_html)?;
121        Ok(MayUnsupported::Ok(()))
122    }
123
124    fn report_modified(
125        &self,
126        name: &str,
127        diff: BinaryDiff,
128        reporter: &HtmlReport,
129    ) -> Result<MayUnsupported<()>, Self::Error> {
130        let diff_changes = diff.changes();
131        let stat = BinaryDiff::stat(&diff_changes);
132        let preview_html = BinaryPreviewTemplate {
133            body: BinaryPreviewBody::Modified {
134                expected_size: diff.expected().len(),
135                actual_size: diff.actual().len(),
136                added_bytes: stat.added,
137                deleted_bytes: stat.deleted,
138            },
139        };
140        let detail_html = BinaryDetailTemplate {
141            detail: BinaryDetailBody::Diff {
142                expected: diff.expected(),
143                actual: diff.actual(),
144                diff: &diff_changes,
145            },
146        };
147        reporter.record_modified(name, COMPARES_NAME, preview_html, detail_html)?;
148        Ok(MayUnsupported::Ok(()))
149    }
150
151    fn report_added(
152        &self,
153        name: &str,
154        data: FileLeaf,
155        reporter: &HtmlReport,
156    ) -> Result<MayUnsupported<()>, Self::Error> {
157        let preview_html = BinaryPreviewTemplate {
158            body: BinaryPreviewBody::Single {
159                size: data.content.len(),
160            },
161        };
162        let detail_html = BinaryDetailTemplate {
163            detail: BinaryDetailBody::Single {
164                label: "added",
165                body: &data.content,
166            },
167        };
168        reporter.record_added(name, COMPARES_NAME, preview_html, detail_html)?;
169        Ok(MayUnsupported::Ok(()))
170    }
171
172    fn report_deleted(
173        &self,
174        name: &str,
175        data: FileLeaf,
176        reporter: &HtmlReport,
177    ) -> Result<MayUnsupported<()>, Self::Error> {
178        let preview_html = BinaryPreviewTemplate {
179            body: BinaryPreviewBody::Single {
180                size: data.content.len(),
181            },
182        };
183        let detail_html = BinaryDetailTemplate {
184            detail: BinaryDetailBody::Single {
185                label: "deleted",
186                body: &data.content,
187            },
188        };
189        reporter.record_deleted(name, COMPARES_NAME, preview_html, detail_html)?;
190        Ok(MayUnsupported::Ok(()))
191    }
192}