shell_tunnel/output/
sanitizer.rs1use vte::{Params, Parser, Perform};
4
5pub struct OutputSanitizer;
7
8impl OutputSanitizer {
9 pub fn strip_ansi(input: &[u8]) -> String {
13 let mut extractor = PlainTextExtractor::new();
14 let mut parser = Parser::new();
15
16 parser.advance(&mut extractor, input);
17
18 extractor.into_string()
19 }
20
21 pub fn strip_ansi_str(input: &str) -> String {
23 Self::strip_ansi(input.as_bytes())
24 }
25}
26
27struct PlainTextExtractor {
29 output: Vec<u8>,
30}
31
32impl PlainTextExtractor {
33 fn new() -> Self {
34 Self { output: Vec::new() }
35 }
36
37 fn into_string(self) -> String {
38 String::from_utf8_lossy(&self.output).into_owned()
39 }
40}
41
42impl Perform for PlainTextExtractor {
43 fn print(&mut self, c: char) {
44 let mut buf = [0u8; 4];
45 let encoded = c.encode_utf8(&mut buf);
46 self.output.extend_from_slice(encoded.as_bytes());
47 }
48
49 fn execute(&mut self, byte: u8) {
50 match byte {
52 0x0A | 0x0D | 0x09 => self.output.push(byte),
54 _ => {}
56 }
57 }
58
59 fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {
60 }
62
63 fn put(&mut self, _byte: u8) {
64 }
66
67 fn unhook(&mut self) {
68 }
70
71 fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {
72 }
74
75 fn csi_dispatch(
76 &mut self,
77 _params: &Params,
78 _intermediates: &[u8],
79 _ignore: bool,
80 _action: char,
81 ) {
82 }
84
85 fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {
86 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_plain_text() {
96 let input = b"hello world";
97 let output = OutputSanitizer::strip_ansi(input);
98 assert_eq!(output, "hello world");
99 }
100
101 #[test]
102 fn test_strip_color_codes() {
103 let input = b"\x1b[31mred\x1b[0m";
105 let output = OutputSanitizer::strip_ansi(input);
106 assert_eq!(output, "red");
107 }
108
109 #[test]
110 fn test_strip_bold() {
111 let input = b"\x1b[1mbold\x1b[0m";
113 let output = OutputSanitizer::strip_ansi(input);
114 assert_eq!(output, "bold");
115 }
116
117 #[test]
118 fn test_preserve_newlines() {
119 let input = b"line1\nline2\nline3";
120 let output = OutputSanitizer::strip_ansi(input);
121 assert_eq!(output, "line1\nline2\nline3");
122 }
123
124 #[test]
125 fn test_strip_cursor_movement() {
126 let input = b"\x1b[2J\x1b[Hcontent";
128 let output = OutputSanitizer::strip_ansi(input);
129 assert_eq!(output, "content");
130 }
131
132 #[test]
133 fn test_complex_sequence() {
134 let input = b"\x1b[32m\x1b[1mGreen Bold\x1b[0m Normal \x1b[34mBlue\x1b[0m";
136 let output = OutputSanitizer::strip_ansi(input);
137 assert_eq!(output, "Green Bold Normal Blue");
138 }
139
140 #[test]
141 fn test_osc_title() {
142 let input = b"\x1b]0;Window Title\x07actual content";
144 let output = OutputSanitizer::strip_ansi(input);
145 assert_eq!(output, "actual content");
146 }
147
148 #[test]
149 fn test_strip_ansi_str() {
150 let input = "\x1b[31mcolored\x1b[0m";
151 let output = OutputSanitizer::strip_ansi_str(input);
152 assert_eq!(output, "colored");
153 }
154
155 #[test]
156 fn test_preserve_tabs() {
157 let input = b"col1\tcol2\tcol3";
158 let output = OutputSanitizer::strip_ansi(input);
159 assert_eq!(output, "col1\tcol2\tcol3");
160 }
161
162 #[test]
163 fn test_empty_input() {
164 let output = OutputSanitizer::strip_ansi(b"");
165 assert_eq!(output, "");
166 }
167
168 #[test]
169 fn test_only_escape_codes() {
170 let input = b"\x1b[31m\x1b[0m\x1b[2J";
171 let output = OutputSanitizer::strip_ansi(input);
172 assert_eq!(output, "");
173 }
174}