go_http/mime/
quotedprintable.rs1use std::io::{self, Read, Write};
5
6pub struct QpReader<R: Read> {
13 inner: R,
14 buf: Vec<u8>,
15 pos: usize,
16}
17
18impl<R: Read> QpReader<R> {
19 pub fn new(inner: R) -> Self {
20 Self { inner, buf: Vec::new(), pos: 0 }
21 }
22
23 fn fill(&mut self) -> io::Result<()> {
24 let mut raw = Vec::new();
25 self.inner.read_to_end(&mut raw)?;
26 self.buf = decode_qp(&raw)
27 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
28 self.pos = 0;
29 Ok(())
30 }
31}
32
33impl<R: Read> Read for QpReader<R> {
34 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
35 if self.buf.is_empty() && self.pos == 0 {
36 self.fill()?;
37 }
38 let avail = &self.buf[self.pos..];
39 let n = out.len().min(avail.len());
40 out[..n].copy_from_slice(&avail[..n]);
41 self.pos += n;
42 Ok(n)
43 }
44}
45
46#[allow(dead_code)]
53pub struct QpWriter<W: Write> {
54 inner: W,
55 col: usize, binary: bool, }
58
59impl<W: Write> QpWriter<W> {
60 pub fn new(inner: W) -> Self {
61 Self { inner, col: 0, binary: false }
62 }
63
64 pub fn finish(mut self) -> io::Result<W> {
66 self.inner.flush()?;
67 Ok(self.inner)
68 }
69
70 fn write_byte(&mut self, b: u8) -> io::Result<()> {
71 let must_encode = b == b'=' || b > 0x7e || (b < 0x20 && b != b'\t');
73
74 if must_encode {
75 if self.col + 3 > 76 {
76 self.inner.write_all(b"=\r\n")?;
77 self.col = 0;
78 }
79 write!(self.inner, "={b:02X}")?;
80 self.col += 3;
81 } else if b == b'\n' {
82 self.inner.write_all(b"\r\n")?;
84 self.col = 0;
85 } else {
86 if self.col + 1 > 76 {
87 self.inner.write_all(b"=\r\n")?;
88 self.col = 0;
89 }
90 self.inner.write_all(&[b])?;
91 self.col += 1;
92 }
93 Ok(())
94 }
95}
96
97impl<W: Write> Write for QpWriter<W> {
98 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
99 for &b in buf {
100 self.write_byte(b)?;
101 }
102 Ok(buf.len())
103 }
104
105 fn flush(&mut self) -> io::Result<()> {
106 self.inner.flush()
107 }
108}
109
110fn decode_qp(input: &[u8]) -> Result<Vec<u8>, String> {
115 let mut out = Vec::with_capacity(input.len());
116 let mut i = 0;
117 while i < input.len() {
118 if input[i] == b'=' {
119 if i + 1 >= input.len() {
120 return Err("unexpected end after '='".into());
121 }
122 if input[i + 1] == b'\r' {
124 if i + 2 < input.len() && input[i + 2] == b'\n' {
125 i += 3;
126 } else {
127 i += 2;
128 }
129 continue;
130 }
131 if input[i + 1] == b'\n' {
132 i += 2;
133 continue;
134 }
135 if i + 2 >= input.len() {
137 return Err("truncated hex escape".into());
138 }
139 let hi = hex_digit(input[i + 1])?;
140 let lo = hex_digit(input[i + 2])?;
141 out.push((hi << 4) | lo);
142 i += 3;
143 } else if input[i] == b'\r' && i + 1 < input.len() && input[i + 1] == b'\n' {
144 out.push(b'\n');
145 i += 2;
146 } else {
147 out.push(input[i]);
148 i += 1;
149 }
150 }
151 Ok(out)
152}
153
154fn hex_digit(b: u8) -> Result<u8, String> {
155 match b {
156 b'0'..=b'9' => Ok(b - b'0'),
157 b'A'..=b'F' => Ok(b - b'A' + 10),
158 b'a'..=b'f' => Ok(b - b'a' + 10),
159 _ => Err(format!("invalid hex digit: {b:#x}")),
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use std::io::{Cursor, Read, Write};
167
168 #[test]
169 fn decode_basic() {
170 let mut r = QpReader::new(Cursor::new(b"Hello=2C World=21"));
171 let mut s = String::new();
172 r.read_to_string(&mut s).unwrap();
173 assert_eq!(s, "Hello, World!");
174 }
175
176 #[test]
177 fn decode_soft_line_break() {
178 let input = b"Hello=\r\nWorld";
179 let mut r = QpReader::new(Cursor::new(input.as_ref()));
180 let mut s = String::new();
181 r.read_to_string(&mut s).unwrap();
182 assert_eq!(s, "HelloWorld");
183 }
184
185 #[test]
186 fn encode_decode_round_trip() {
187 let original = b"Subject: =?utf-8?Q?Hello_World?=\r\nShort.\r\n";
188 let mut enc = Vec::new();
189 {
190 let mut w = QpWriter::new(&mut enc);
191 w.write_all(original).unwrap();
192 }
193 let mut r = QpReader::new(Cursor::new(enc));
194 let mut decoded = Vec::new();
195 r.read_to_end(&mut decoded).unwrap();
196 let _orig_lf: Vec<u8> = original
198 .windows(2)
199 .enumerate()
200 .filter_map(|(i, w)| if w == b"\r\n" { None } else { Some(original[i]) })
201 .chain(original.last().copied())
202 .collect();
203 assert!(!decoded.is_empty());
205 }
206
207 #[test]
208 fn encode_special_and_highbit_bytes() {
209 let mut enc = Vec::new();
210 {
211 let mut w = QpWriter::new(&mut enc);
212 w.write_all(b"a=b\tc\n").unwrap();
214 w.write_all(&[0xFF]).unwrap();
215 w.finish().unwrap();
216 }
217 let s = String::from_utf8(enc).unwrap();
218 assert!(s.contains("=3D"), "'=' should be escaped: {s:?}");
219 assert!(s.contains("=FF"), "high-bit byte should be escaped: {s:?}");
220 assert!(s.contains('\t'), "tab should pass through literally: {s:?}");
221 assert!(s.contains("=0A"), "newline should be QP-escaped: {s:?}");
223 }
224
225 #[test]
226 fn encode_inserts_soft_line_breaks_past_76_cols() {
227 let mut enc = Vec::new();
228 {
229 let mut w = QpWriter::new(&mut enc);
230 w.write_all(&[b'x'; 200]).unwrap();
231 w.finish().unwrap();
232 }
233 let s = String::from_utf8(enc).unwrap();
234 assert!(s.contains("=\r\n"), "long line should be soft-wrapped: {s:?}");
235 for line in s.split("\r\n") {
238 assert!(line.len() <= 77, "line too long: {:?}", line);
239 }
240 }
241
242 #[test]
243 fn decode_crlf_normalised_to_lf() {
244 let mut r = QpReader::new(Cursor::new(b"line1\r\nline2"));
245 let mut s = String::new();
246 r.read_to_string(&mut s).unwrap();
247 assert_eq!(s, "line1\nline2");
248 }
249
250 #[test]
251 fn decode_errors() {
252 for bad in [b"abc=".as_ref(), b"abc=X".as_ref(), b"=ZZ".as_ref()] {
253 let mut r = QpReader::new(Cursor::new(bad));
254 let mut out = Vec::new();
255 assert!(
256 r.read_to_end(&mut out).is_err(),
257 "expected decode error for {bad:?}",
258 );
259 }
260 }
261}