1pub mod charsets;
14pub mod html;
15mod prefix;
16pub mod preview;
17
18use crate::Encoding;
19use encodify::{base64, qp};
20use std::borrow::Cow;
21
22pub(crate) use prefix::{Limit, TextPrefix, truncate};
23
24impl Encoding {
25 pub(crate) fn parse(value: &[u8]) -> Option<Encoding> {
26 let token = value
27 .split(|&byte| byte == b'(')
28 .next()
29 .unwrap_or_default()
30 .trim_ascii();
31 hashify::map_ignore_case!(token, Encoding,
32 "7bit" => Encoding::None,
33 "8bit" => Encoding::None,
34 "binary" => Encoding::None,
35 "base64" => Encoding::Base64,
36 "quoted-printable" => Encoding::QuotedPrintable,
37 )
38 .copied()
39 }
40
41 pub fn decode(self, bytes: &[u8]) -> Cow<'_, [u8]> {
43 match self {
44 Encoding::None => Cow::Borrowed(bytes),
45 Encoding::QuotedPrintable => qp::BODY.decode(bytes).unwrap_or(Cow::Borrowed(bytes)),
46 Encoding::Base64 => {
47 let mut out = Vec::new();
48 self.decode_append(bytes, &mut out);
49 Cow::Owned(out)
50 }
51 }
52 }
53
54 pub fn decode_checked(self, bytes: &[u8]) -> (Cow<'_, [u8]>, bool) {
56 match self {
57 Encoding::None => (Cow::Borrowed(bytes), false),
58 Encoding::QuotedPrintable => match qp::BODY.strict().decode(bytes) {
59 Ok(decoded) => (decoded, false),
60 Err(_) => (self.decode(bytes), true),
61 },
62 Encoding::Base64 => {
63 let mut out = Vec::new();
64 let malformed = base64::MIME.decode_append(bytes, &mut out).is_err();
65 if malformed {
66 base64_tolerant(bytes, &mut out);
67 }
68 (Cow::Owned(out), malformed)
69 }
70 }
71 }
72
73 pub fn decode_append(self, bytes: &[u8], out: &mut Vec<u8>) {
75 match self {
76 Encoding::None => out.extend_from_slice(bytes),
77 Encoding::QuotedPrintable => {
78 if qp::BODY.decode_append(bytes, out).is_err() {
79 out.extend_from_slice(bytes);
80 }
81 }
82 Encoding::Base64 => {
83 if base64::MIME.decode_append(bytes, out).is_err() {
84 base64_tolerant(bytes, out);
85 }
86 }
87 }
88 }
89
90 pub fn decoded_len(self, bytes: &[u8]) -> usize {
92 match self {
93 Encoding::None => bytes.len(),
94 Encoding::Base64 => base64::MIME.decoded_len(bytes).unwrap_or_else(|_| {
95 let mut out = Vec::new();
96 base64_tolerant(bytes, &mut out);
97 out.len()
98 }),
99 Encoding::QuotedPrintable => qp::BODY.decoded_len(bytes).unwrap_or(bytes.len()),
100 }
101 }
102}
103
104fn base64_tolerant(rest: &[u8], out: &mut Vec<u8>) {
105 base64_feed(rest, out).finish(out);
106}
107
108fn base64_feed(mut rest: &[u8], out: &mut Vec<u8>) -> base64::Decoder {
109 let mut decoder = base64::MIME.decoder();
110 loop {
111 rest = rest
112 .get(decoder.decode_symbols(rest, out)..)
113 .unwrap_or_default();
114 match rest.split_first() {
115 None => return decoder,
116 Some((b'=', tail)) => {
117 decoder.pad(out);
118 rest = tail;
119 }
120 Some((_, tail)) => rest = tail,
121 }
122 }
123}
124
125pub(crate) fn rewrite<'x>(
126 text: Cow<'x, str>,
127 rewrite: impl for<'a> FnOnce(&'a str) -> Cow<'a, str>,
128) -> Cow<'x, str> {
129 match text {
130 Cow::Borrowed(text) => rewrite(text),
131 Cow::Owned(text) => {
132 let rewritten = match rewrite(&text) {
133 Cow::Owned(rewritten) => Some(rewritten),
134 Cow::Borrowed(same) if same.len() == text.len() => None,
135 Cow::Borrowed(other) => Some(other.to_owned()),
136 };
137 Cow::Owned(rewritten.unwrap_or(text))
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::{Charset, scan::tests::Rng};
146
147 #[test]
148 fn qp_matches_codec() {
149 let alphabet = b"ab \t\r\n=\xc3\xa9";
150 let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
151 for _ in 0..20_000 {
152 let len = rng.below(24);
153 let input: Vec<u8> = (0..len)
154 .map(|_| {
155 alphabet
156 .get(rng.below(alphabet.len()))
157 .copied()
158 .unwrap_or(b'a')
159 })
160 .collect();
161 let mut decoded = Vec::new();
162 qp::BODY
163 .decode_append(&input, &mut decoded)
164 .expect("never fails");
165 let borrowed = Encoding::QuotedPrintable.decode(&input);
166 assert_eq!(borrowed.as_ref(), decoded.as_slice(), "{input:?}");
167 if matches!(borrowed, Cow::Borrowed(_)) {
168 assert_eq!(decoded, input);
169 }
170 assert_eq!(Encoding::QuotedPrintable.decoded_len(&input), decoded.len());
171 }
172 assert!(matches!(
173 Encoding::QuotedPrintable.decode(b"plain text\r\nline two\nend"),
174 Cow::Borrowed(_)
175 ));
176 assert!(matches!(
177 Encoding::QuotedPrintable.decode(b"trailing \r\n"),
178 Cow::Owned(_)
179 ));
180 }
181
182 #[test]
183 fn base64_best_effort() {
184 assert_eq!(Encoding::Base64.decode(b"SGVs\r\nbG8=").as_ref(), b"Hello");
185 assert_eq!(Encoding::Base64.decode(b"SGVs-bG8=").as_ref(), b"Hello");
186 assert_eq!(Encoding::Base64.decode(b"VGVzdA").as_ref(), b"Test");
187 assert_eq!(Encoding::Base64.decoded_len(b"SGVs-bG8="), 5);
188 assert_eq!(Encoding::Base64.decoded_len(b"SGVsbG8="), 5);
189 }
190
191 #[test]
192 fn checked_transfer_decoding() {
193 let alphabet = b"ab \t\r\n=\xc3\xa9AZ09+/-";
194 let mut rng = Rng(0x7ab5_c0de_1234_9876);
195 for _ in 0..20_000 {
196 let len = rng.below(24);
197 let input: Vec<u8> = (0..len)
198 .map(|_| {
199 alphabet
200 .get(rng.below(alphabet.len()))
201 .copied()
202 .unwrap_or(b'a')
203 })
204 .collect();
205 for encoding in [Encoding::None, Encoding::QuotedPrintable, Encoding::Base64] {
206 let (checked, malformed) = encoding.decode_checked(&input);
207 assert_eq!(checked, encoding.decode(&input), "{input:?}");
208 let expected = match encoding {
209 Encoding::None => false,
210 Encoding::QuotedPrintable => qp::BODY.strict().decoded_len(&input).is_err(),
211 Encoding::Base64 => base64::MIME.decoded_len(&input).is_err(),
212 };
213 assert_eq!(malformed, expected, "{encoding:?} {input:?}");
214 }
215 }
216 for (input, encoding, decoded, malformed) in [
217 (&b"SGVs\r\nbG8="[..], Encoding::Base64, &b"Hello"[..], false),
218 (b"SGVs-bG8=", Encoding::Base64, b"Hello", true),
219 (b"VGVzdA", Encoding::Base64, b"Test", false),
220 (
221 b"caf=C3=A9",
222 Encoding::QuotedPrintable,
223 b"caf\xc3\xa9",
224 false,
225 ),
226 (
227 b"soft=\r\nbreak",
228 Encoding::QuotedPrintable,
229 b"softbreak",
230 false,
231 ),
232 (b"a=ZZb", Encoding::QuotedPrintable, b"a=ZZb", true),
233 (b"end=4", Encoding::QuotedPrintable, b"end=4", true),
234 (b"as is=", Encoding::None, b"as is=", false),
235 ] {
236 let (checked, problem) = encoding.decode_checked(input);
237 assert_eq!(
238 (checked.as_ref(), problem),
239 (decoded, malformed),
240 "{input:?}"
241 );
242 }
243 }
244
245 #[test]
246 fn known_transfer_encodings() {
247 for (value, encoding) in [
248 (&b"7bit"[..], Encoding::None),
249 (b" 8BIT\r\n", Encoding::None),
250 (b"binary (comment)", Encoding::None),
251 (b"\r\n base64\r\n", Encoding::Base64),
252 (b" BASE64 (comment)\n", Encoding::Base64),
253 (b" Quoted-Printable", Encoding::QuotedPrintable),
254 ] {
255 assert_eq!(Encoding::parse(value), Some(encoding), "{value:?}");
256 }
257 for value in [
258 &b"x-uuencode"[..],
259 b"",
260 b" base64;",
261 b"7-bit",
262 b"\"base64\"",
263 ] {
264 assert_eq!(Encoding::parse(value), None, "{value:?}");
265 }
266 }
267
268 #[test]
269 fn text_conversion() {
270 let label = |label: &str| Charset::from_label(label.as_bytes()).unwrap_or_default();
271 assert!(matches!(
272 label("utf-8").decode_cow(Cow::Borrowed(b"caf\xc3\xa9")),
273 Cow::Borrowed("café")
274 ));
275 assert_eq!(
276 label("iso-8859-1").decode_cow(Cow::Borrowed(b"caf\xe9")),
277 "café"
278 );
279 assert_eq!(
280 Charset::default().decode_cow(Cow::Owned(b"caf\xe9".to_vec())),
281 "caf\u{fffd}"
282 );
283 assert_eq!(
284 label("x-unknown").decode_cow(Cow::Owned(b"caf\xe9".to_vec())),
285 "caf\u{fffd}"
286 );
287 }
288}