Skip to main content

aft/compress/
generic.rs

1use crate::compress::{CompressionResult, Compressor};
2
3pub fn strip_ansi(input: &str) -> String {
4    let bytes = input.as_bytes();
5    let mut output = String::with_capacity(input.len());
6    let mut index = 0;
7    let mut last_kept = 0;
8
9    while index < bytes.len() {
10        if bytes[index] != 0x1b {
11            index += 1;
12            continue;
13        }
14
15        let Some(next) = bytes.get(index + 1).copied() else {
16            break;
17        };
18
19        let end = if next == b'[' {
20            let mut cursor = index + 2;
21            while cursor < bytes.len() {
22                if (0x40..=0x7e).contains(&bytes[cursor]) {
23                    cursor += 1;
24                    break;
25                }
26                cursor += 1;
27            }
28            cursor
29        } else if (0x40..=0x5f).contains(&next) {
30            index + 2
31        } else {
32            index += 1;
33            continue;
34        };
35
36        output.push_str(&input[last_kept..index]);
37        index = end.min(bytes.len());
38        last_kept = index;
39    }
40
41    output.push_str(&input[last_kept..]);
42    output
43}
44
45pub fn dedup_consecutive(input: &str) -> String {
46    let had_trailing_newline = input.ends_with('\n');
47    let mut output = String::with_capacity(input.len());
48    let mut lines = input.lines();
49
50    let Some(mut current) = lines.next() else {
51        return String::new();
52    };
53    let mut count = 1usize;
54
55    for line in lines {
56        if line == current {
57            count += 1;
58        } else {
59            push_dedup_run(&mut output, current, count);
60            current = line;
61            count = 1;
62        }
63    }
64    push_dedup_run(&mut output, current, count);
65
66    if !had_trailing_newline {
67        output.pop();
68    }
69
70    output
71}
72
73fn push_dedup_run(output: &mut String, line: &str, count: usize) {
74    output.push_str(line);
75    output.push('\n');
76    if count >= 4 {
77        output.push_str("... (");
78        output.push_str(&(count - 1).to_string());
79        output.push_str(" more)\n");
80    } else {
81        for _ in 1..count {
82            output.push_str(line);
83            output.push('\n');
84        }
85    }
86}
87
88pub fn middle_truncate(
89    input: &str,
90    threshold_bytes: usize,
91    keep_head: usize,
92    keep_tail: usize,
93) -> String {
94    if input.len() <= threshold_bytes {
95        return input.to_string();
96    }
97
98    let head_end = floor_char_boundary(input, keep_head.min(input.len()));
99    let tail_start = ceil_char_boundary(input, input.len().saturating_sub(keep_tail));
100
101    if head_end >= tail_start {
102        return input.to_string();
103    }
104
105    let truncated_bytes = tail_start - head_end;
106    let mut output = String::with_capacity(head_end + keep_tail + 64);
107    output.push_str(&input[..head_end]);
108    if !output.ends_with('\n') {
109        output.push('\n');
110    }
111    output.push_str("...<truncated ");
112    output.push_str(&truncated_bytes.to_string());
113    output.push_str(" bytes>...\n");
114    output.push_str(&input[tail_start..]);
115    output
116}
117
118pub(crate) fn floor_char_boundary(input: &str, mut index: usize) -> usize {
119    while index > 0 && !input.is_char_boundary(index) {
120        index -= 1;
121    }
122    index
123}
124
125pub(crate) fn ceil_char_boundary(input: &str, mut index: usize) -> usize {
126    while index < input.len() && !input.is_char_boundary(index) {
127        index += 1;
128    }
129    index
130}
131
132pub struct GenericCompressor;
133
134impl GenericCompressor {
135    pub fn compress_output(output: &str) -> String {
136        let stripped = strip_ansi(output);
137        dedup_consecutive(&stripped)
138    }
139}
140
141impl Compressor for GenericCompressor {
142    fn matches(&self, _command: &str) -> bool {
143        true
144    }
145
146    fn compress(&self, _command: &str, output: &str) -> CompressionResult {
147        Self::compress_output(output).into()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn generic_does_not_pretruncate_above_old_five_kib_threshold() {
157        let input = (0..900)
158            .map(|idx| format!("unique-line-{idx:04}"))
159            .collect::<Vec<_>>()
160            .join("\n");
161        assert!(input.len() > 5 * 1024);
162
163        let compressed = GenericCompressor::compress_output(&input);
164
165        assert!(!compressed.contains("...<truncated "));
166        assert!(compressed.len() > 5 * 1024);
167        assert!(compressed.contains("unique-line-0000"));
168        assert!(compressed.contains("unique-line-0899"));
169    }
170}