Skip to main content

otp/encoding/
base32.rs

1//! Base32 encoding/decoding without padding, using [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-6) alphabet.
2
3const BASE32_ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
4
5/// # Example
6/// ```rust
7/// use otp::encoding::base32;
8///
9/// let bytes = b"any + old & data";
10/// let encoded = base32::encode(bytes);
11/// assert_eq!("MFXHSIBLEBXWYZBAEYQGIYLUME", encoded.as_str());
12/// ```
13pub fn encode(data: &[u8]) -> String {
14    let mut encoded = String::with_capacity((data.len() * 8).div_ceil(5));
15
16    let mut buffer = 0_u16;
17    let mut bits_left = 0;
18
19    for &byte in data {
20        buffer <<= 8;
21        buffer |= byte as u16;
22        bits_left += 8;
23
24        while bits_left >= 5 {
25            let index = (buffer >> (bits_left - 5)) & 0x1F;
26            encoded.push(BASE32_ALPHABET[index as usize] as char);
27            bits_left -= 5;
28        }
29    }
30
31    if bits_left > 0 {
32        let index = (buffer << (5 - bits_left)) & 0x1F;
33        encoded.push(BASE32_ALPHABET[index as usize] as char);
34    }
35
36    encoded
37}
38
39/// # Example:
40/// ```rust
41/// use otp::encoding::base32;
42///
43/// let hello_world = "JBSWY3DPFQQHO33SNRSCC";
44/// let decoded = base32::decode(hello_world).expect("Decoding failed");
45/// assert_eq!(b"Hello, world!", decoded.as_slice());
46/// assert_eq!(hello_world, base32::encode(b"Hello, world!"));
47///
48/// let hello_world_with_pad = "JBSWY3DPFQQHO33SNRSCC===";
49/// let result_invalid_err = base32::decode(hello_world_with_pad);
50/// assert!(matches!(result_invalid_err, Err(base32::DecodeBase32Error::InvalidChar(_))));
51/// ```
52pub fn decode(data: &str) -> Result<Vec<u8>, DecodeBase32Error> {
53    let mut output = Vec::with_capacity((data.len() * 5) / 8);
54
55    let mut buffer = 0_u32;
56    let mut bits_left = 0;
57
58    for b in data.bytes() {
59        let val = match b {
60            b'A'..=b'Z' => b - b'A',
61            b'a'..=b'z' => b - b'a',
62            b'2'..=b'7' => b - b'2' + 26,
63            _ => return Err(DecodeBase32Error::InvalidChar(b as char)),
64        } as u32;
65
66        buffer = (buffer << 5) | val;
67        bits_left += 5;
68
69        if bits_left >= 8 {
70            output.push((buffer >> (bits_left - 8)) as u8);
71            bits_left -= 8;
72        }
73    }
74
75    Ok(output)
76}
77
78#[derive(Debug, Clone)]
79pub enum DecodeBase32Error {
80    InvalidChar(char),
81}
82
83impl std::fmt::Display for DecodeBase32Error {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            DecodeBase32Error::InvalidChar(c) => write!(f, "invalid base32 character: '{c}'"),
87        }
88    }
89}
90
91impl std::error::Error for DecodeBase32Error {}