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}