1pub fn is_csi_final_byte(byte: u8) -> bool {
3 (0x40..=0x7e).contains(&byte)
4}
5
6pub fn strip_ansi(input: &str) -> String {
9 let mut out = String::with_capacity(input.len());
10 let mut chars = input.chars().peekable();
11 while let Some(character) = chars.next() {
12 if character != '\x1b' {
13 out.push(character);
14 continue;
15 }
16 match chars.next() {
17 Some('[') => {
18 for next in chars.by_ref() {
19 if next.is_ascii() && is_csi_final_byte(next as u8) {
20 break;
21 }
22 }
23 }
24 Some(kind @ (']' | 'P' | 'X' | '^' | '_')) => {
25 while let Some(next) = chars.next() {
26 if kind == ']' && next == '\x07' {
27 break;
28 }
29 if next == '\x1b' && chars.peek() == Some(&'\\') {
30 chars.next();
31 break;
32 }
33 }
34 }
35 Some(' '..='/') => {
36 for next in chars.by_ref() {
37 if ('0'..='~').contains(&next) {
38 break;
39 }
40 }
41 }
42 _ => {}
43 }
44 }
45 out
46}
47
48pub fn truncate_utf8(value: &mut String, limit: usize) {
50 if value.len() <= limit {
51 return;
52 }
53 let mut end = limit;
54 while !value.is_char_boundary(end) {
55 end -= 1;
56 }
57 value.truncate(end);
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn ansi_controls_do_not_swallow_text_after_punctuation_finals() {
66 assert_eq!(strip_ansi("a\x1b[1~after\x1b[2@done"), "aafterdone");
67 assert_eq!(strip_ansi("\x1b[31mλ warning\x1b[0m\n\t"), "λ warning\n\t");
68 assert_eq!(
69 strip_ansi("\x1b]8;;https://example.invalid\x1b\\link\x1b]8;;\x1b\\"),
70 "link"
71 );
72 assert_eq!(strip_ansi("a\x1b]title\x07b\x1bPpayload\x1b\\c"), "abc");
73 assert_eq!(strip_ansi("a\x1b[31"), "a");
74 assert_eq!(strip_ansi("a\x1b]unfinished"), "a");
75 assert_eq!(strip_ansi("a\x1b"), "a");
76 assert_eq!(strip_ansi("\x1b(Btext"), "text");
77 }
78
79 #[test]
80 fn truncation_covers_every_multibyte_boundary() {
81 for (limit, expected) in [
82 (0, ""),
83 (1, "a"),
84 (2, "a"),
85 (3, "aλ"),
86 (4, "aλ"),
87 (7, "aλ🙂"),
88 (99, "aλ🙂z"),
89 ] {
90 let mut value = "aλ🙂z".to_owned();
91 truncate_utf8(&mut value, limit);
92 assert_eq!(value, expected);
93 }
94 }
95}
96
97pub fn push_bounded(output: &mut String, value: &str, maximum_bytes: usize) -> bool {
99 let available = maximum_bytes.saturating_sub(output.len());
100 if value.len() <= available {
101 output.push_str(value);
102 return true;
103 }
104 let mut keep = available.min(value.len());
105 while keep > 0 && !value.is_char_boundary(keep) {
106 keep -= 1;
107 }
108 output.push_str(&value[..keep]);
109 false
110}
111
112pub fn append_truncation_marker(output: &mut String, marker: &str, maximum_bytes: usize) {
114 if marker.len() > maximum_bytes {
115 return;
116 }
117 if output.len().saturating_add(marker.len()) > maximum_bytes {
118 let mut keep = maximum_bytes - marker.len();
119 while keep > 0 && !output.is_char_boundary(keep) {
120 keep -= 1;
121 }
122 output.truncate(keep);
123 }
124 output.push_str(marker);
125}
126
127pub fn utf8_prefix(value: &str, limit: usize) -> &str {
129 let mut end = limit.min(value.len());
130 while !value.is_char_boundary(end) {
131 end -= 1;
132 }
133 &value[..end]
134}
135
136#[cfg(test)]
137mod bounded_tests {
138 use super::*;
139 #[test]
140 fn bounded_append_and_marker_share_the_exact_byte_budget() {
141 let mut output = "a".to_owned();
142 assert!(!push_bounded(&mut output, "λ🙂", 4));
143 assert_eq!(output, "aλ");
144 append_truncation_marker(&mut output, "!", 3);
145 assert_eq!(output, "a!");
146 append_truncation_marker(&mut output, "too long", 3);
147 assert_eq!(output, "a!");
148 assert!(push_bounded(&mut output, "b", 3));
149 assert_eq!(output, "a!b");
150 assert_eq!(utf8_prefix("λ🙂", usize::MAX), "λ🙂");
151 }
152}