1#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub(crate) struct VisFlags {
6 pub octal: bool,
8 pub cstyle: bool,
10 pub tab: bool,
12 pub newline: bool,
14 pub safe: bool,
16 pub noslash: bool,
18}
19
20#[must_use]
22pub(crate) fn encode_bytes(input: &[u8], flags: VisFlags) -> String {
23 let mut encoded = String::new();
24 for &byte in input {
25 encode_byte(byte, flags, &mut encoded);
26 }
27 encoded
28}
29
30#[must_use]
32pub(crate) fn encode_str(input: &str, flags: VisFlags) -> String {
33 encode_bytes(input.as_bytes(), flags)
34}
35
36#[must_use]
38pub(crate) fn encode_buffer_sample(input: &[u8]) -> String {
39 const WIDTH: usize = 200;
40
41 let flags = VisFlags {
42 octal: true,
43 cstyle: true,
44 tab: true,
45 newline: true,
46 safe: false,
47 noslash: false,
48 };
49
50 let prefix_len = input.len().min(WIDTH);
51 let mut encoded = encode_bytes(&input[..prefix_len], flags);
52 if input.len() > WIDTH || encoded.len() > WIDTH {
53 truncate_at_byte_boundary(&mut encoded, WIDTH);
54 encoded.push_str("...");
55 }
56 encoded
57}
58
59#[must_use]
61pub fn encode_paste_bytes(input: &[u8]) -> Vec<u8> {
62 let mut encoded = Vec::with_capacity(input.len());
63 for &byte in input {
64 encode_paste_byte(byte, &mut encoded);
65 }
66 encoded
67}
68
69fn encode_paste_byte(byte: u8, output: &mut Vec<u8>) {
70 if (0x20..=0x7e).contains(&byte) || byte >= 0x80 {
71 output.push(byte);
72 return;
73 }
74
75 match byte {
76 b'\0' => output.extend_from_slice(b"\\000"),
77 b'\x07' => output.extend_from_slice(b"\\a"),
78 b'\x08' => output.extend_from_slice(b"\\b"),
79 b'\t' => output.extend_from_slice(b"\\t"),
80 b'\n' => output.extend_from_slice(b"\\n"),
81 b'\x0b' => output.extend_from_slice(b"\\v"),
82 b'\x0c' => output.extend_from_slice(b"\\f"),
83 b'\r' => output.extend_from_slice(b"\\r"),
84 _ => {
85 output.push(b'\\');
86 output.push(b'0' + ((byte >> 6) & 0x7));
87 output.push(b'0' + ((byte >> 3) & 0x7));
88 output.push(b'0' + (byte & 0x7));
89 }
90 }
91}
92
93fn encode_byte(byte: u8, flags: VisFlags, output: &mut String) {
94 if should_keep_raw(byte, flags) {
95 output.push(char::from(byte));
96 return;
97 }
98
99 if flags.cstyle {
100 match byte {
101 b'\0' => {
102 output.push_str("\\000");
103 return;
104 }
105 b'\x07' => {
106 output.push_str("\\a");
107 return;
108 }
109 b'\x08' => {
110 output.push_str("\\b");
111 return;
112 }
113 b'\x09' => {
114 output.push_str("\\t");
115 return;
116 }
117 b'\x0a' => {
118 output.push_str("\\n");
119 return;
120 }
121 b'\x0b' => {
122 output.push_str("\\v");
123 return;
124 }
125 b'\x0c' => {
126 output.push_str("\\f");
127 return;
128 }
129 b'\x0d' => {
130 output.push_str("\\r");
131 return;
132 }
133 b'\\' if !flags.noslash => {
134 output.push_str("\\\\");
135 return;
136 }
137 _ => {}
138 }
139 }
140
141 if byte == b'\\' && !flags.noslash {
142 output.push_str("\\\\");
143 return;
144 }
145
146 if flags.octal || flags.safe || flags.cstyle {
147 output.push('\\');
148 output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
149 output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
150 output.push(char::from(b'0' + (byte & 0x7)));
151 return;
152 }
153
154 output.push(char::from(byte));
155}
156
157fn should_keep_raw(byte: u8, flags: VisFlags) -> bool {
158 if byte == b'\\' && !flags.noslash {
159 return false;
160 }
161
162 if flags.safe {
163 return (0x20..=0x7e).contains(&byte);
164 }
165
166 if byte == b'\t' {
167 return !flags.tab;
168 }
169 if byte == b'\n' {
170 return !flags.newline;
171 }
172
173 (0x20..=0x7e).contains(&byte)
174}
175
176fn truncate_at_byte_boundary(value: &mut String, max_len: usize) {
177 if value.len() <= max_len {
178 return;
179 }
180
181 let mut boundary = max_len;
182 while !value.is_char_boundary(boundary) {
183 boundary -= 1;
184 }
185 value.truncate(boundary);
186}
187
188#[cfg(test)]
189mod tests {
190 use super::{encode_buffer_sample, encode_bytes, encode_paste_bytes, VisFlags};
191
192 #[test]
193 fn sample_encoding_matches_tmux_style_for_common_controls() {
194 let encoded = encode_buffer_sample(b"one\two\nthree\\");
195 assert_eq!(encoded, "one\\two\\nthree\\\\");
196 }
197
198 #[test]
199 fn sample_encoding_truncates_to_tmux_width() {
200 let input = vec![b'a'; 205];
201 let encoded = encode_buffer_sample(&input);
202 assert_eq!(encoded.len(), 203);
203 assert!(encoded.ends_with("..."));
204 }
205
206 #[test]
207 fn safe_paste_encoding_keeps_printable_bytes() {
208 assert_eq!(encode_paste_bytes(b"hello"), b"hello");
209 }
210
211 #[test]
212 fn safe_paste_encoding_escapes_controls_without_backslash_escaping() {
213 let encoded = String::from_utf8(encode_paste_bytes(b"\t\\\x1b")).expect("utf8");
214 assert_eq!(encoded, "\\t\\\\033");
215 }
216
217 #[test]
218 fn safe_paste_encoding_preserves_utf8_bytes() {
219 let input = "echo PASTED_漢字".as_bytes();
220 assert_eq!(encode_paste_bytes(input), input);
221 }
222
223 #[test]
224 fn safe_paste_encoding_escapes_delete() {
225 let encoded = String::from_utf8(encode_paste_bytes(b"\x7f")).expect("utf8");
226 assert_eq!(encoded, "\\177");
227 }
228
229 #[test]
230 fn generic_encoding_respects_noslash() {
231 let encoded = encode_bytes(
232 b"\\",
233 VisFlags {
234 noslash: true,
235 ..VisFlags::default()
236 },
237 );
238 assert_eq!(encoded, "\\");
239 }
240}