Skip to main content

pamoja_gateway/
base64.rs

1//! The base64 encoding of RFC 4648, which the protocol carries every radio payload in.
2//!
3//! The packet forwarder protocol asks for padded base64 on the way up and allows it to be
4//! omitted on the way down, and gateways in the field send both. [`encode`] pads, as the
5//! protocol's own examples do for uplinks, and [`decode`] accepts a string with or without
6//! padding, so a datagram from either side is read the same way.
7//!
8//! # Examples
9//!
10//! ```
11//! use pamoja_gateway::base64;
12//!
13//! assert_eq!(base64::encode(b"foobar"), "Zm9vYmFy");
14//! assert_eq!(base64::encode(b"fo"), "Zm8=");
15//! assert_eq!(base64::decode("Zm8=").as_deref(), Ok(&b"fo"[..]));
16//! assert_eq!(base64::decode("Zm8").as_deref(), Ok(&b"fo"[..]));
17//! ```
18
19use std::fmt;
20
21/// The alphabet of RFC 4648 section 4, in the order the standard gives it.
22const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
23
24/// The character that pads the last group to four.
25const PAD: u8 = b'=';
26
27/// Why a string is not base64.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum DecodeError {
30    /// A character outside the alphabet, with its position.
31    Character {
32        /// Where it is in the string, counted in bytes.
33        at: usize,
34        /// The byte itself.
35        byte: u8,
36    },
37    /// A group of one character, which no number of bytes encodes to.
38    Length(usize),
39    /// Padding before the end of the string, at this position.
40    Padding(usize),
41}
42
43impl fmt::Display for DecodeError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            DecodeError::Character { at, byte } => {
47                write!(f, "byte {at} is {byte:#04x}, which is not base64")
48            }
49            DecodeError::Length(len) => {
50                write!(
51                    f,
52                    "{len} base64 characters leave a group of one, which encodes nothing"
53                )
54            }
55            DecodeError::Padding(at) => write!(f, "padding at byte {at} is before the end"),
56        }
57    }
58}
59
60impl std::error::Error for DecodeError {}
61
62/// Encodes bytes as padded base64.
63///
64/// # Arguments
65///
66/// * `bytes` - the bytes to encode.
67///
68/// # Returns
69///
70/// The base64 string, padded to a multiple of four characters.
71pub fn encode(bytes: &[u8]) -> String {
72    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
73    for group in bytes.chunks(3) {
74        let mut packed = 0u32;
75        for (index, byte) in group.iter().enumerate() {
76            packed |= u32::from(*byte) << (16 - 8 * index);
77        }
78        let characters = group.len() + 1;
79        for index in 0..characters {
80            let sextet = (packed >> (18 - 6 * index)) & 0x3F;
81            out.push(char::from(ALPHABET[sextet as usize]));
82        }
83        for _ in characters..4 {
84            out.push(char::from(PAD));
85        }
86    }
87    out
88}
89
90/// Decodes base64, with or without its padding.
91///
92/// # Arguments
93///
94/// * `text` - the base64 string.
95///
96/// # Returns
97///
98/// The bytes it encodes.
99///
100/// # Errors
101///
102/// Returns [`DecodeError`] for a character outside the alphabet, padding before the end, or a
103/// trailing group of one character.
104pub fn decode(text: &str) -> Result<Vec<u8>, DecodeError> {
105    let bytes = text.as_bytes();
106    let body = match bytes.iter().position(|byte| *byte == PAD) {
107        Some(at) => {
108            if bytes[at..].iter().any(|byte| *byte != PAD) || bytes.len() - at > 2 {
109                return Err(DecodeError::Padding(at));
110            }
111            &bytes[..at]
112        }
113        None => bytes,
114    };
115    if body.len() % 4 == 1 {
116        return Err(DecodeError::Length(body.len()));
117    }
118
119    let mut out = Vec::with_capacity(body.len() / 4 * 3);
120    for (group, characters) in body.chunks(4).enumerate() {
121        let mut packed = 0u32;
122        for (index, byte) in characters.iter().enumerate() {
123            let sextet = sextet(*byte).ok_or(DecodeError::Character {
124                at: group * 4 + index,
125                byte: *byte,
126            })?;
127            packed |= u32::from(sextet) << (18 - 6 * index);
128        }
129        for index in 0..characters.len() - 1 {
130            out.push(((packed >> (16 - 8 * index)) & 0xFF) as u8);
131        }
132    }
133    Ok(out)
134}
135
136/// Returns the value of a base64 character.
137fn sextet(byte: u8) -> Option<u8> {
138    ALPHABET
139        .iter()
140        .position(|character| *character == byte)
141        .map(|value| value as u8)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    /// The test vectors of RFC 4648 section 10.
149    const VECTORS: [(&str, &str); 7] = [
150        ("", ""),
151        ("f", "Zg=="),
152        ("fo", "Zm8="),
153        ("foo", "Zm9v"),
154        ("foob", "Zm9vYg=="),
155        ("fooba", "Zm9vYmE="),
156        ("foobar", "Zm9vYmFy"),
157    ];
158
159    #[test]
160    fn the_rfc_vectors_encode_and_decode() {
161        for (plain, encoded) in VECTORS {
162            assert_eq!(encode(plain.as_bytes()), encoded, "encoding {plain:?}");
163            assert_eq!(
164                decode(encoded).expect("the vector decodes"),
165                plain.as_bytes(),
166                "decoding {encoded:?}"
167            );
168        }
169    }
170
171    #[test]
172    fn padding_is_optional_on_the_way_in() {
173        for (plain, encoded) in VECTORS {
174            let stripped = encoded.trim_end_matches('=');
175            assert_eq!(
176                decode(stripped).expect("an unpadded vector decodes"),
177                plain.as_bytes(),
178                "decoding {stripped:?}"
179            );
180        }
181    }
182
183    #[test]
184    fn the_protocols_own_example_payload_decodes() {
185        // From the PUSH_DATA example of the protocol, which sends its payload padded.
186        let heard = decode("VEVTVF9QQUNLRVRfMTIzNA==").expect("the example decodes");
187        assert_eq!(heard, b"TEST_PACKET_1234");
188    }
189
190    #[test]
191    fn every_byte_survives_a_round_trip() {
192        let all: Vec<u8> = (0..=255).collect();
193        for len in 0..=all.len() {
194            let bytes = &all[..len];
195            let round = decode(&encode(bytes)).expect("what we encoded decodes");
196            assert_eq!(round, bytes, "{len} bytes");
197        }
198    }
199
200    #[test]
201    fn what_is_not_base64_is_refused() {
202        assert_eq!(
203            decode("Zm9v!g=="),
204            Err(DecodeError::Character { at: 4, byte: b'!' })
205        );
206        assert_eq!(decode("Zg=a"), Err(DecodeError::Padding(2)));
207        assert_eq!(decode("Zm9vY"), Err(DecodeError::Length(5)));
208        assert!(decode("Zm9vYg==").is_ok(), "a padded group is still good");
209    }
210}