Skip to main content

hardless_core/
base64.rs

1//! Standard base64 (RFC 4648, with padding) for byte payloads in bridge messages.
2
3use bytes::Bytes;
4use serde::{de, Deserialize, Deserializer, Serializer};
5
6const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
8/// Encodes bytes as padded standard base64.
9///
10/// ```
11/// assert_eq!(hardless_core::base64::encode(b"\x1b@"), "G0A=");
12/// ```
13pub fn encode(data: &[u8]) -> String {
14    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
15    for chunk in data.chunks(3) {
16        let b = [
17            chunk[0],
18            chunk.get(1).copied().unwrap_or(0),
19            chunk.get(2).copied().unwrap_or(0),
20        ];
21        let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
22        for i in 0..4 {
23            if i <= chunk.len() {
24                out.push(char::from(ALPHABET[((n >> (18 - 6 * i)) & 0x3f) as usize]));
25            } else {
26                out.push('=');
27            }
28        }
29    }
30    out
31}
32
33/// Decodes standard base64; padding is optional, whitespace is not allowed.
34///
35/// ```
36/// assert_eq!(hardless_core::base64::decode("G0A=").unwrap(), b"\x1b@");
37/// assert!(hardless_core::base64::decode("G0A?").is_err());
38/// ```
39pub fn decode(text: &str) -> Result<Vec<u8>, &'static str> {
40    fn value(c: u8) -> Result<u32, &'static str> {
41        match c {
42            b'A'..=b'Z' => Ok(u32::from(c - b'A')),
43            b'a'..=b'z' => Ok(u32::from(c - b'a') + 26),
44            b'0'..=b'9' => Ok(u32::from(c - b'0') + 52),
45            b'+' => Ok(62),
46            b'/' => Ok(63),
47            _ => Err("invalid base64 character"),
48        }
49    }
50    let trimmed = text.trim_end_matches('=');
51    if text.len() - trimmed.len() > 2 {
52        return Err("too much base64 padding");
53    }
54    let bytes = trimmed.as_bytes();
55    if bytes.len() % 4 == 1 {
56        return Err("invalid base64 length");
57    }
58    let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
59    for chunk in bytes.chunks(4) {
60        let mut n = 0u32;
61        for (i, &c) in chunk.iter().enumerate() {
62            n |= value(c)? << (18 - 6 * i);
63        }
64        let [_, b0, b1, b2] = n.to_be_bytes();
65        out.push(b0);
66        if chunk.len() > 2 {
67            out.push(b1);
68        }
69        if chunk.len() > 3 {
70            out.push(b2);
71        }
72    }
73    Ok(out)
74}
75
76/// `#[serde(with = "crate::base64::serde_bytes")]` for [`Bytes`] fields.
77pub(crate) mod serde_bytes {
78    use super::*;
79
80    pub(crate) fn serialize<S: Serializer>(data: &Bytes, s: S) -> Result<S::Ok, S::Error> {
81        s.serialize_str(&encode(data))
82    }
83
84    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Bytes, D::Error> {
85        let text = <std::borrow::Cow<'de, str>>::deserialize(d)?;
86        decode(&text).map(Bytes::from).map_err(de::Error::custom)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn round_trips_every_length() {
96        for len in 0..40usize {
97            let data: Vec<u8> = (0..len).map(|i| (i * 37 % 256) as u8).collect();
98            assert_eq!(decode(&encode(&data)).unwrap(), data, "length {len}");
99        }
100        assert_eq!(encode(b"Man"), "TWFu");
101        assert_eq!(encode(b"Ma"), "TWE=");
102        assert_eq!(encode(b"M"), "TQ==");
103        assert_eq!(decode("TQ").unwrap(), b"M");
104        assert!(decode("T").is_err());
105    }
106}