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 flags = VisFlags {
63 octal: true,
64 cstyle: true,
65 tab: true,
66 newline: true,
67 safe: true,
68 noslash: true,
69 };
70 encode_bytes(input, flags).into_bytes()
71}
72
73fn encode_byte(byte: u8, flags: VisFlags, output: &mut String) {
74 if should_keep_raw(byte, flags) {
75 output.push(char::from(byte));
76 return;
77 }
78
79 if flags.cstyle {
80 match byte {
81 b'\0' => {
82 output.push_str("\\000");
83 return;
84 }
85 b'\x07' => {
86 output.push_str("\\a");
87 return;
88 }
89 b'\x08' => {
90 output.push_str("\\b");
91 return;
92 }
93 b'\x09' => {
94 output.push_str("\\t");
95 return;
96 }
97 b'\x0a' => {
98 output.push_str("\\n");
99 return;
100 }
101 b'\x0b' => {
102 output.push_str("\\v");
103 return;
104 }
105 b'\x0c' => {
106 output.push_str("\\f");
107 return;
108 }
109 b'\x0d' => {
110 output.push_str("\\r");
111 return;
112 }
113 b'\\' if !flags.noslash => {
114 output.push_str("\\\\");
115 return;
116 }
117 _ => {}
118 }
119 }
120
121 if byte == b'\\' && !flags.noslash {
122 output.push_str("\\\\");
123 return;
124 }
125
126 if flags.octal || flags.safe || flags.cstyle {
127 output.push('\\');
128 output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
129 output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
130 output.push(char::from(b'0' + (byte & 0x7)));
131 return;
132 }
133
134 output.push(char::from(byte));
135}
136
137fn should_keep_raw(byte: u8, flags: VisFlags) -> bool {
138 if byte == b'\\' && !flags.noslash {
139 return false;
140 }
141
142 if flags.safe {
143 return (0x20..=0x7e).contains(&byte);
144 }
145
146 if byte == b'\t' {
147 return !flags.tab;
148 }
149 if byte == b'\n' {
150 return !flags.newline;
151 }
152
153 (0x20..=0x7e).contains(&byte)
154}
155
156fn truncate_at_byte_boundary(value: &mut String, max_len: usize) {
157 if value.len() <= max_len {
158 return;
159 }
160
161 let mut boundary = max_len;
162 while !value.is_char_boundary(boundary) {
163 boundary -= 1;
164 }
165 value.truncate(boundary);
166}
167
168#[cfg(test)]
169mod tests {
170 use super::{encode_buffer_sample, encode_bytes, encode_paste_bytes, VisFlags};
171
172 #[test]
173 fn sample_encoding_matches_tmux_style_for_common_controls() {
174 let encoded = encode_buffer_sample(b"one\two\nthree\\");
175 assert_eq!(encoded, "one\\two\\nthree\\\\");
176 }
177
178 #[test]
179 fn sample_encoding_truncates_to_tmux_width() {
180 let input = vec![b'a'; 205];
181 let encoded = encode_buffer_sample(&input);
182 assert_eq!(encoded.len(), 203);
183 assert!(encoded.ends_with("..."));
184 }
185
186 #[test]
187 fn safe_paste_encoding_keeps_printable_bytes() {
188 assert_eq!(encode_paste_bytes(b"hello"), b"hello");
189 }
190
191 #[test]
192 fn safe_paste_encoding_escapes_controls_without_backslash_escaping() {
193 let encoded = String::from_utf8(encode_paste_bytes(b"\t\\\x1b")).expect("utf8");
194 assert_eq!(encoded, "\\t\\\\033");
195 }
196
197 #[test]
198 fn generic_encoding_respects_noslash() {
199 let encoded = encode_bytes(
200 b"\\",
201 VisFlags {
202 noslash: true,
203 ..VisFlags::default()
204 },
205 );
206 assert_eq!(encoded, "\\");
207 }
208}