cloudiful_redactor/
input.rs1use 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}