Skip to main content

codetics_ocpp_ws/
base64.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! Standard Base64 (RFC 4648 §4) with padding.
5
6const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
8pub fn encode(input: &[u8]) -> String {
9    let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
10    for chunk in input.chunks(3) {
11        let b = [
12            chunk[0],
13            *chunk.get(1).unwrap_or(&0),
14            *chunk.get(2).unwrap_or(&0),
15        ];
16        let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
17        out.push(ALPHABET[(n >> 18) as usize & 63] as char);
18        out.push(ALPHABET[(n >> 12) as usize & 63] as char);
19        out.push(if chunk.len() > 1 {
20            ALPHABET[(n >> 6) as usize & 63] as char
21        } else {
22            '='
23        });
24        out.push(if chunk.len() > 2 {
25            ALPHABET[n as usize & 63] as char
26        } else {
27            '='
28        });
29    }
30    out
31}
32
33pub fn decode(input: &str) -> Option<Vec<u8>> {
34    let s = input.trim().as_bytes();
35    if !s.len().is_multiple_of(4) {
36        return None;
37    }
38    let val = |c: u8| -> Option<u32> {
39        Some(match c {
40            b'A'..=b'Z' => c - b'A',
41            b'a'..=b'z' => c - b'a' + 26,
42            b'0'..=b'9' => c - b'0' + 52,
43            b'+' => 62,
44            b'/' => 63,
45            _ => return None,
46        } as u32)
47    };
48    let mut out = Vec::with_capacity(s.len() / 4 * 3);
49    let chunks = s.len() / 4;
50    for (i, c) in s.as_chunks::<4>().0.iter().enumerate() {
51        let last = i + 1 == chunks;
52        let pad = if last {
53            c.iter().rev().take_while(|&&b| b == b'=').count()
54        } else {
55            0
56        };
57        if pad > 2 || (!last && c.contains(&b'=')) {
58            return None;
59        }
60        let mut n = 0u32;
61        for (j, &b) in c.iter().enumerate() {
62            let v = if j >= 4 - pad { 0 } else { val(b)? };
63            n = n << 6 | v;
64        }
65        out.push((n >> 16) as u8);
66        if pad < 2 {
67            out.push((n >> 8) as u8);
68        }
69        if pad < 1 {
70            out.push(n as u8);
71        }
72    }
73    Some(out)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn rfc4648_vectors() {
82        for (raw, enc) in [
83            ("", ""),
84            ("f", "Zg=="),
85            ("fo", "Zm8="),
86            ("foo", "Zm9v"),
87            ("foob", "Zm9vYg=="),
88            ("fooba", "Zm9vYmE="),
89            ("foobar", "Zm9vYmFy"),
90        ] {
91            assert_eq!(encode(raw.as_bytes()), enc);
92            assert_eq!(decode(enc).unwrap(), raw.as_bytes());
93        }
94        assert!(decode("Zm9").is_none());
95        assert!(decode("Z=9v").is_none());
96        assert!(decode("Zm9v!A==").is_none());
97    }
98
99    #[test]
100    fn ocpp_basic_auth_example() {
101        // OCPP-J 1.6 §6.2.2: identity "AL1000", 20-byte key 0001020304050607FFFFFFFFFFFFFFFFFFFFFFFF.
102        let mut creds = b"AL1000:".to_vec();
103        creds.extend_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7]);
104        creds.extend_from_slice(&[0xFF; 12]);
105        assert_eq!(encode(&creds), "QUwxMDAwOgABAgMEBQYH////////////////");
106    }
107}