Skip to main content

cloudiful_redactor/
input.rs

1use serde::{Deserialize, Serialize};
2use std::ops::Range;
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum InputKind {
7    #[default]
8    Text,
9    GitDiff,
10}
11
12pub(crate) fn redactable_ranges(text: &str, input_kind: InputKind) -> Vec<Range<usize>> {
13    match input_kind {
14        InputKind::Text => std::iter::once(0..text.len()).collect(),
15        InputKind::GitDiff => git_diff_redactable_ranges(text),
16    }
17}
18
19fn git_diff_redactable_ranges(text: &str) -> Vec<Range<usize>> {
20    let mut ranges = Vec::new();
21    let mut offset = 0;
22    let mut in_hunk = false;
23    let mut in_binary_patch = false;
24
25    for line in text.split_inclusive('\n') {
26        if line.starts_with("diff --git ") {
27            in_hunk = false;
28            in_binary_patch = false;
29            offset += line.len();
30            continue;
31        }
32
33        if line.starts_with("GIT binary patch") || line.starts_with("Binary files ") {
34            in_hunk = false;
35            in_binary_patch = true;
36            offset += line.len();
37            continue;
38        }
39
40        if in_binary_patch {
41            offset += line.len();
42            continue;
43        }
44
45        if line.starts_with("@@") {
46            in_hunk = true;
47            offset += line.len();
48            continue;
49        }
50
51        if !in_hunk {
52            offset += line.len();
53            continue;
54        }
55
56        if line.starts_with("\\ No newline at end of file") {
57            offset += line.len();
58            continue;
59        }
60
61        if matches!(
62            line.as_bytes().first(),
63            Some(b'+') | Some(b'-') | Some(b' ')
64        ) && line.len() > 1
65        {
66            ranges.push((offset + 1)..(offset + line.len()));
67        }
68
69        offset += line.len();
70    }
71
72    ranges
73}
74
75#[cfg(test)]
76mod tests {
77    use super::{InputKind, redactable_ranges};
78
79    #[test]
80    fn git_diff_ranges_only_cover_hunk_lines() {
81        let diff = concat!(
82            "diff --git a/config.yml b/config.yml\n",
83            "index 1111111..2222222 100644\n",
84            "--- a/config.yml\n",
85            "+++ b/config.yml\n",
86            "@@ -1,2 +1,3 @@\n",
87            "-old_secret=abc123\n",
88            "+new_secret=def456\n",
89            " unchanged=value\n",
90        );
91
92        let ranges = redactable_ranges(diff, InputKind::GitDiff);
93        let covered = ranges
94            .into_iter()
95            .map(|range| &diff[range])
96            .collect::<Vec<_>>();
97
98        assert_eq!(
99            covered,
100            vec![
101                "old_secret=abc123\n",
102                "new_secret=def456\n",
103                "unchanged=value\n"
104            ]
105        );
106    }
107
108    #[test]
109    fn text_mode_covers_entire_input() {
110        let text = "plain text";
111        let ranges = redactable_ranges(text, InputKind::Text);
112        assert_eq!(ranges.len(), 1);
113        assert_eq!(&text[ranges[0].clone()], text);
114    }
115}