email_encoding/body/
base64.rs

1//! Base64 email body encoder.
2
3use core::{
4    fmt::{self, Write},
5    str,
6};
7
8use ::base64::Engine;
9
10const LINE_LEN: usize = 76;
11const CRLF: &str = "\r\n";
12
13/// Base64 encode the provided bytes.
14///
15/// Splits the provided `b` into 57 bytes chunks and
16/// base64 encodes them, writing the resulting 76 characters
17/// CRLF sequence into `w`.
18///
19/// The last line may be less than 76 characters in length
20/// and will not end in CRLF.
21///
22/// # Examples
23///
24/// ```rust
25/// # fn main() -> core::fmt::Result {
26/// let input = "Hello!
27/// You've got mail!
28/// This one is base64 encoded.
29///
30/// Enjoy your bytes 📬📬📬";
31///
32/// let mut output = String::new();
33/// email_encoding::body::base64::encode(input.as_bytes(), &mut output)?;
34/// assert_eq!(
35///     output,
36///     concat!(
37///         "SGVsbG8hCllvdSd2ZSBnb3QgbWFpbCEKVGhpcyBvbmUgaXMgYmFzZTY0IGVuY29kZWQuCgpFbmpv\r\n",
38///         "eSB5b3VyIGJ5dGVzIPCfk6zwn5Os8J+TrA=="
39///     )
40/// );
41/// # Ok(())
42/// # }
43/// ```
44pub fn encode(b: &[u8], w: &mut dyn Write) -> fmt::Result {
45    let mut buf = [0; LINE_LEN];
46
47    let mut chunks = b.chunks(LINE_LEN / 4 * 3).peekable();
48    while let Some(chunk) = chunks.next() {
49        let len = ::base64::engine::general_purpose::STANDARD
50            .encode_slice(chunk, &mut buf)
51            .expect("base64 output `buf` is not big enough");
52
53        w.write_str(str::from_utf8(&buf[..len]).expect("base64 produced an invalid encode"))?;
54        if chunks.peek().is_some() {
55            w.write_str(CRLF)?;
56        }
57    }
58
59    Ok(())
60}
61
62/// Predict how many bytes [`encode`] is going to write given a `input_len` input length.
63///
64/// # Panics
65///
66/// Panics if any of the internal calculations overflow.
67///
68/// # Examples
69///
70/// ```rust
71/// # use email_encoding::body::base64::encoded_len;
72/// assert_eq!(encoded_len(0), 0);
73/// assert_eq!(encoded_len(16), 24);
74/// assert_eq!(encoded_len(300), 410);
75/// ```
76pub const fn encoded_len(input_len: usize) -> usize {
77    // FIXME: use `Option::expect` with MSRV >= 1.83
78    macro_rules! checked {
79        ($val:expr) => {
80            match $val {
81                Some(val) => val,
82                None => panic!("overflow"),
83            }
84        };
85    }
86
87    let mut base64_len = checked!((input_len / 3).checked_mul(4));
88    if input_len % 3 != 0 {
89        base64_len = checked!(base64_len.checked_add(4));
90    }
91    let mut crlf_len = base64_len / LINE_LEN * CRLF.len();
92    if crlf_len >= CRLF.len() && base64_len % LINE_LEN == 0 {
93        crlf_len -= CRLF.len();
94    }
95    checked!(base64_len.checked_add(crlf_len))
96}
97
98#[cfg(test)]
99mod tests {
100    use alloc::string::String;
101
102    use pretty_assertions::assert_eq;
103
104    use super::{encode, encoded_len};
105
106    #[test]
107    fn empty() {
108        let input = b"";
109        let mut output = String::new();
110
111        encode(input, &mut output).unwrap();
112
113        assert_eq!(output, "");
114        assert_eq!(output.len(), encoded_len(input.len()));
115    }
116
117    #[test]
118    fn oneline() {
119        let input = b"012";
120        let mut output = String::new();
121
122        encode(input, &mut output).unwrap();
123
124        assert_eq!(output, "MDEy");
125        assert_eq!(output.len(), encoded_len(input.len()));
126    }
127
128    #[test]
129    fn oneline_padded() {
130        let input = b"0123";
131        let mut output = String::new();
132
133        encode(input, &mut output).unwrap();
134
135        assert_eq!(output, "MDEyMw==");
136        assert_eq!(output.len(), encoded_len(input.len()));
137    }
138
139    #[test]
140    fn multiline() {
141        let input =
142            b"012345678998765432100123456789987654321001234567899876543210012345678998765432100";
143        let mut output = String::new();
144
145        encode(input, &mut output).unwrap();
146
147        assert_eq!(
148            output,
149            concat!(
150                "MDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2NTQz\r\n",
151                "MjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTAw"
152            )
153        );
154        assert_eq!(output.len(), encoded_len(input.len()));
155    }
156
157    #[test]
158    fn multiline_padded() {
159        let input =
160            b"01234567899876543210012345678998765432100123456789987654321001234567899876543210";
161        let mut output = String::new();
162
163        encode(input, &mut output).unwrap();
164
165        assert_eq!(
166            output,
167            concat!(
168                "MDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2NTQz\r\n",
169                "MjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTA="
170            )
171        );
172        assert_eq!(output.len(), encoded_len(input.len()));
173    }
174
175    #[test]
176    fn multiline_exact() {
177        let input =
178            b"012345678998765432100123456789987654321001234567899876543210012345678998765432100123456789987654321001234567899876543210012345678998765432100123456789987654321001234567899";
179        let mut output = String::new();
180
181        encode(input, &mut output).unwrap();
182
183        assert_eq!(
184            output,
185            concat!(
186                "MDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2NTQz\r\n",
187                "MjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5ODc2\r\n",
188                "NTQzMjEwMDEyMzQ1Njc4OTk4NzY1NDMyMTAwMTIzNDU2Nzg5OTg3NjU0MzIxMDAxMjM0NTY3ODk5"
189            )
190        );
191        assert_eq!(output.len(), encoded_len(input.len()));
192    }
193
194    #[test]
195    #[should_panic(expected = "overflow")]
196    fn overflow() {
197        encoded_len(usize::MAX);
198    }
199}