semdiff_differ_text/
lib.rs1use memmap2::Mmap;
2use mime::Mime;
3use semdiff_core::fs::FileLeaf;
4use semdiff_core::{Diff, DiffCalculator, MayUnsupported};
5use similar::TextDiffConfig;
6use std::convert;
7use std::sync::Arc;
8
9pub mod report_html;
10pub mod report_json;
11pub mod report_summary;
12
13#[cfg(test)]
14mod tests;
15
16pub struct TextDiffReporter;
17
18#[derive(Debug)]
19pub struct TextDiff {
20 equal: bool,
21 expected: Arc<Mmap>,
22 actual: Arc<Mmap>,
23}
24
25impl Diff for TextDiff {
26 fn equal(&self) -> bool {
27 self.equal
28 }
29}
30
31impl TextDiff {
32 fn diff(&self) -> similar::TextDiff<'_, '_, [u8]> {
33 text_diff_lines(&self.expected[..], &self.actual[..])
34 }
35}
36
37fn text_diff_lines<'a>(expected: &'a [u8], actual: &'a [u8]) -> similar::TextDiff<'a, 'a, [u8]> {
38 TextDiffConfig::default()
39 .algorithm(similar::Algorithm::Patience)
40 .diff_lines(expected, actual)
41}
42
43fn is_printable_text(text: &str) -> bool {
44 text.chars()
45 .all(|ch| !ch.is_control() || ch.is_ascii_whitespace() || (!ch.is_ascii() && ch.is_whitespace()))
46}
47
48fn is_text_file(kind: &Mime, body: &[u8]) -> bool {
49 let Ok(text) = str::from_utf8(body) else {
50 return false;
51 };
52
53 if is_text_mime(kind) {
54 return true;
55 }
56
57 is_printable_text(text)
58}
59
60fn is_text_mime(kind: &Mime) -> bool {
61 kind.type_() == mime::TEXT
62 || matches!(
63 kind.essence_str(),
64 "application/json"
65 | "application/xml"
66 | "application/javascript"
67 | "application/x-javascript"
68 | "application/x-www-form-urlencoded"
69 | "application/yaml"
70 | "application/x-yaml"
71 | "application/toml"
72 )
73}
74
75#[derive(Default)]
76pub struct TextDiffCalculator;
77
78impl DiffCalculator<FileLeaf> for TextDiffCalculator {
79 type Error = convert::Infallible;
80 type Diff = TextDiff;
81
82 fn diff(
83 &self,
84 _name: &str,
85 expected: FileLeaf,
86 actual: FileLeaf,
87 ) -> Result<MayUnsupported<Self::Diff>, Self::Error> {
88 'available: {
89 let Ok(expected_str) = str::from_utf8(&expected.content) else {
90 return Ok(MayUnsupported::Unsupported);
91 };
92 let Ok(actual_str) = str::from_utf8(&actual.content) else {
93 return Ok(MayUnsupported::Unsupported);
94 };
95
96 if is_text_mime(&expected.kind) && is_text_mime(&actual.kind) {
97 break 'available;
98 }
99
100 if !is_printable_text(expected_str) || !is_printable_text(actual_str) {
101 return Ok(MayUnsupported::Unsupported);
102 }
103 }
104 Ok(MayUnsupported::Ok(TextDiff {
105 equal: <[u8] as PartialEq<[u8]>>::eq(&expected.content, &actual.content),
106 expected: expected.content,
107 actual: actual.content,
108 }))
109 }
110}