Skip to main content

aft/compress/
generic.rs

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