Skip to main content

go_http/mime/
quotedprintable.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Quoted-printable codec — port of Go's `mime/quotedprintable`.
4use std::io::{self, Read, Write};
5
6// ---------------------------------------------------------------------------
7// QpReader — decodes quoted-printable
8// ---------------------------------------------------------------------------
9
10/// Decodes quoted-printable encoded data from the wrapped reader.
11/// Port of Go's `quotedprintable.NewReader`.
12pub 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// ---------------------------------------------------------------------------
47// QpWriter — encodes to quoted-printable
48// ---------------------------------------------------------------------------
49
50/// Encodes data written to it as quoted-printable.
51/// Port of Go's `quotedprintable.NewWriter`.
52#[allow(dead_code)]
53pub struct QpWriter<W: Write> {
54    inner:  W,
55    col:    usize,   // current column (0-based)
56    binary: bool,    // if true, encode CR/LF as =0D/=0A
57}
58
59impl<W: Write> QpWriter<W> {
60    pub fn new(inner: W) -> Self {
61        Self { inner, col: 0, binary: false }
62    }
63
64    /// Flush any pending soft line-break and return the inner writer.
65    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        // Must encode: non-printable, '=', or high-bit.
72        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            // Strip trailing whitespace before CRLF (per RFC 2045).
83            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
110// ---------------------------------------------------------------------------
111// decode_qp — core decoder
112// ---------------------------------------------------------------------------
113
114fn 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            // Soft line break: "=\r\n" or "=\n"
123            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            // Hex escape: "=XX"
136            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        // The decoded result normalises CRLF → LF.
197        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        // Just check no data is lost (lengths comparable).
204        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            // '=' must be escaped; 0xFF is high-bit; \t is literal; \n → CRLF.
213            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        // '\n' is a control byte, so the encoder escapes it as =0A.
222        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        // Each wrapped segment carries up to 76 data chars plus the trailing
236        // soft-break '='.
237        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}