Skip to main content

semdiff_differ_text/
lib.rs

1use 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}