Skip to main content

pith_digest/
base64.rs

1//! Base64 (RFC 4648), the standard `A-Za-z0-9+/` alphabet with `=`
2//! padding.
3//!
4//! Both directions write into caller-owned buffers and never
5//! allocate, matching the crate's hex encoding: [`base64_encode`]
6//! needs `encoded_len(input.len())` bytes of output, [`base64_decode`]
7//! at most `encoded.len() / 4 * 3`. Decoding is strict and canonical
8//! (the RFC 4648 §3.5 recommendation): the input length must be a
9//! multiple of four with correct padding, only alphabet characters may
10//! appear, and the discarded trailing bits of the last character must
11//! be zero.
12
13use crate::error::{Error, Result};
14
15/// The 64 RFC 4648 alphabet characters, indexed by 6-bit value.
16const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
17
18/// The encoded length of `len` bytes: four characters per three-byte
19/// group, padded.
20///
21/// ```
22/// assert_eq!(pith_digest::base64::encoded_len(0), 0);
23/// assert_eq!(pith_digest::base64::encoded_len(1), 4);
24/// assert_eq!(pith_digest::base64::encoded_len(3), 4);
25/// assert_eq!(pith_digest::base64::encoded_len(6), 8);
26/// ```
27#[must_use]
28pub const fn encoded_len(len: usize) -> usize {
29    len.div_ceil(3) * 4
30}
31
32/// Encodes `data` into `out` (RFC 4648 §4).
33///
34/// `out` must hold at least [`encoded_len`]`(data.len())` bytes;
35/// exactly that many are written.
36///
37/// ```
38/// use pith_digest::base64::{base64_encode, encoded_len};
39/// let mut out = [0u8; encoded_len(6)];
40/// base64_encode(b"foobar", &mut out).unwrap();
41/// assert_eq!(&out, b"Zm9vYmFy");
42/// ```
43///
44/// # Errors
45///
46/// [`crate::Error::TooLarge`] if `out` is shorter than the encoding
47/// needs.
48pub fn base64_encode(data: &[u8], out: &mut [u8]) -> Result<()> {
49    let needed = encoded_len(data.len());
50    if out.len() < needed {
51        return Err(Error::too_large("base64 output", needed));
52    }
53
54    let mut chunks = data.chunks_exact(3);
55    let mut cursor = 0usize;
56    for chunk in chunks.by_ref() {
57        let n = (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]);
58        out[cursor] = ALPHABET[((n >> 18) & 0x3f) as usize];
59        out[cursor + 1] = ALPHABET[((n >> 12) & 0x3f) as usize];
60        out[cursor + 2] = ALPHABET[((n >> 6) & 0x3f) as usize];
61        out[cursor + 3] = ALPHABET[(n & 0x3f) as usize];
62        cursor += 4;
63    }
64    let rem = chunks.remainder();
65    match rem.len() {
66        0 => {}
67        1 => {
68            let n = u32::from(rem[0]) << 16;
69            out[cursor] = ALPHABET[((n >> 18) & 0x3f) as usize];
70            out[cursor + 1] = ALPHABET[((n >> 12) & 0x3f) as usize];
71            out[cursor + 2] = b'=';
72            out[cursor + 3] = b'=';
73        }
74        _ => {
75            let n = (u32::from(rem[0]) << 16) | (u32::from(rem[1]) << 8);
76            out[cursor] = ALPHABET[((n >> 18) & 0x3f) as usize];
77            out[cursor + 1] = ALPHABET[((n >> 12) & 0x3f) as usize];
78            out[cursor + 2] = ALPHABET[((n >> 6) & 0x3f) as usize];
79            out[cursor + 3] = b'=';
80        }
81    }
82    Ok(())
83}
84
85/// Decodes one RFC 4648 alphabet character, or [`Error::BadValue`].
86fn sextet(byte: u8) -> Result<u32> {
87    let value = match byte {
88        b'A'..=b'Z' => byte - b'A',
89        b'a'..=b'z' => byte - b'a' + 26,
90        b'0'..=b'9' => byte - b'0' + 52,
91        b'+' => 62,
92        b'/' => 63,
93        _ => return Err(Error::BadValue("base64 input")),
94    };
95    Ok(u32::from(value))
96}
97
98/// Decodes canonical padded base64 from `encoded` into `out`,
99/// returning the number of bytes written (RFC 4648 §4).
100///
101/// `out` must hold at least `encoded.len() / 4 * 3` bytes; the
102/// returned count is that minus the padding.
103///
104/// ```
105/// use pith_digest::base64::base64_decode;
106/// let mut out = [0u8; 6];
107/// let n = base64_decode(b"Zm9vYmFy", &mut out).unwrap();
108/// assert_eq!(&out[..n], b"foobar");
109/// ```
110///
111/// # Errors
112///
113/// [`crate::Error::TooLarge`] if `out` is shorter than the worst-case
114/// decode; [`crate::Error::BadValue`] for a non-canonical input (a
115/// character outside the alphabet, a pad character in the body,
116/// non-zero discarded bits, or a length that is not a multiple of
117/// four).
118pub fn base64_decode(encoded: &[u8], out: &mut [u8]) -> Result<usize> {
119    if encoded.len() % 4 != 0 {
120        return Err(Error::BadValue("base64 input"));
121    }
122    let worst = encoded.len() / 4 * 3;
123    if out.len() < worst {
124        return Err(Error::too_large("base64 output", worst));
125    }
126    // `=` may only close the input: one or two pad characters.
127    let pad = match encoded {
128        [.., b'=', b'='] => 2,
129        [.., b'='] => 1,
130        _ => 0,
131    };
132    let body = encoded.len() - pad;
133
134    let mut written = 0usize;
135    let mut acc: u32 = 0;
136    let mut bits: u32 = 0;
137    for (i, &byte) in encoded.iter().enumerate() {
138        if byte == b'=' {
139            // Only the counted suffix may carry pad characters.
140            if i < body {
141                return Err(Error::BadValue("base64 input"));
142            }
143            continue;
144        }
145        acc = (acc << 6) | sextet(byte)?;
146        bits += 6;
147        if bits >= 8 {
148            bits -= 8;
149            out[written] = ((acc >> bits) & 0xff) as u8;
150            written += 1;
151        }
152    }
153    // Canonical trailing bits: a padded group discards two bits per
154    // pad character, and they must be zero (RFC 4648 3.5).
155    if acc & ((1 << bits) - 1) != 0 {
156        return Err(Error::BadValue("base64 input"));
157    }
158    Ok(written)
159}
160
161#[cfg(test)]
162mod tests {
163    use super::{base64_decode, base64_encode, encoded_len};
164
165    /// The RFC 4648 §10 test vectors, both directions.
166    #[test]
167    fn rfc_4648_s10_vectors() {
168        for (plain, encoded) in [
169            (&b""[..], &b""[..]),
170            (b"f", b"Zg=="),
171            (b"fo", b"Zm8="),
172            (b"foo", b"Zm9v"),
173            (b"foob", b"Zm9vYg=="),
174            (b"fooba", b"Zm9vYmE="),
175            (b"foobar", b"Zm9vYmFy"),
176        ] {
177            let mut out = [0u8; 16];
178            base64_encode(plain, &mut out[..encoded_len(plain.len())]).unwrap();
179            assert_eq!(&out[..encoded_len(plain.len())], encoded, "{plain:?}");
180            let mut back = [0u8; 16];
181            let n = base64_decode(encoded, &mut back).unwrap();
182            assert_eq!(&back[..n], plain, "{encoded:?}");
183        }
184    }
185
186    /// Binary round-trips across group boundaries, including every
187    /// 6-bit high value in the last character.
188    #[test]
189    fn binary_round_trips() {
190        for len in 0..64usize {
191            let data: Vec<u8> = (0..len).map(|i| (i * 37 + 11) as u8).collect();
192            let mut out = [0u8; 128];
193            base64_encode(&data, &mut out[..encoded_len(len)]).unwrap();
194            let mut back = [0u8; 128];
195            let n = base64_decode(&out[..encoded_len(len)], &mut back).unwrap();
196            assert_eq!(&back[..n], &data[..], "len {len}");
197        }
198    }
199
200    /// A too-small output buffer is [`crate::Error::TooLarge`].
201    #[test]
202    fn short_output_is_too_large() {
203        let mut out = [0u8; 3];
204        assert!(matches!(
205            base64_encode(b"foobar", &mut out),
206            Err(crate::Error::TooLarge { .. })
207        ));
208        assert!(matches!(
209            base64_decode(b"Zm9vYmFy", &mut out),
210            Err(crate::Error::TooLarge { .. })
211        ));
212    }
213
214    /// Non-canonical inputs are refused: bad characters, inner pads,
215    /// non-zero discarded bits, a bad length, a double pad.
216    #[test]
217    fn non_canonical_inputs_are_refused() {
218        let mut out = [0u8; 16];
219        // A character outside the alphabet.
220        assert!(base64_decode(b"Zm9*YmFy", &mut out).is_err());
221        // A pad character inside the body.
222        assert!(base64_decode(b"Zm=vYmFy", &mut out).is_err());
223        // Non-zero discarded bits ("Zm9vYmFy" with the last character
224        // bumped into the padding zone).
225        assert!(base64_decode(b"Zg+=", &mut out).is_err());
226        // Length not a multiple of four.
227        assert!(base64_decode(b"Zm9", &mut out).is_err());
228        // Two pads claiming two leftover bytes for a 2-byte group.
229        assert!(base64_decode(b"Zg==", &mut out).is_ok());
230        // One pad for a group that needs two.
231        assert!(base64_decode(b"Zg=", &mut out).is_err());
232    }
233}