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}