codetics-ocpp-ws 0.1.0

Codetics OCPP: dependency-light RFC 6455 WebSocket with RFC 7692 permessage-deflate, tuned for many long-lived OCPP connections
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)

//! Standard Base64 (RFC 4648 §4) with padding.

const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

pub fn encode(input: &[u8]) -> String {
    let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
    for chunk in input.chunks(3) {
        let b = [
            chunk[0],
            *chunk.get(1).unwrap_or(&0),
            *chunk.get(2).unwrap_or(&0),
        ];
        let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
        out.push(ALPHABET[(n >> 18) as usize & 63] as char);
        out.push(ALPHABET[(n >> 12) as usize & 63] as char);
        out.push(if chunk.len() > 1 {
            ALPHABET[(n >> 6) as usize & 63] as char
        } else {
            '='
        });
        out.push(if chunk.len() > 2 {
            ALPHABET[n as usize & 63] as char
        } else {
            '='
        });
    }
    out
}

pub fn decode(input: &str) -> Option<Vec<u8>> {
    let s = input.trim().as_bytes();
    if !s.len().is_multiple_of(4) {
        return None;
    }
    let val = |c: u8| -> Option<u32> {
        Some(match c {
            b'A'..=b'Z' => c - b'A',
            b'a'..=b'z' => c - b'a' + 26,
            b'0'..=b'9' => c - b'0' + 52,
            b'+' => 62,
            b'/' => 63,
            _ => return None,
        } as u32)
    };
    let mut out = Vec::with_capacity(s.len() / 4 * 3);
    let chunks = s.len() / 4;
    for (i, c) in s.as_chunks::<4>().0.iter().enumerate() {
        let last = i + 1 == chunks;
        let pad = if last {
            c.iter().rev().take_while(|&&b| b == b'=').count()
        } else {
            0
        };
        if pad > 2 || (!last && c.contains(&b'=')) {
            return None;
        }
        let mut n = 0u32;
        for (j, &b) in c.iter().enumerate() {
            let v = if j >= 4 - pad { 0 } else { val(b)? };
            n = n << 6 | v;
        }
        out.push((n >> 16) as u8);
        if pad < 2 {
            out.push((n >> 8) as u8);
        }
        if pad < 1 {
            out.push(n as u8);
        }
    }
    Some(out)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rfc4648_vectors() {
        for (raw, enc) in [
            ("", ""),
            ("f", "Zg=="),
            ("fo", "Zm8="),
            ("foo", "Zm9v"),
            ("foob", "Zm9vYg=="),
            ("fooba", "Zm9vYmE="),
            ("foobar", "Zm9vYmFy"),
        ] {
            assert_eq!(encode(raw.as_bytes()), enc);
            assert_eq!(decode(enc).unwrap(), raw.as_bytes());
        }
        assert!(decode("Zm9").is_none());
        assert!(decode("Z=9v").is_none());
        assert!(decode("Zm9v!A==").is_none());
    }

    #[test]
    fn ocpp_basic_auth_example() {
        // OCPP-J 1.6 §6.2.2: identity "AL1000", 20-byte key 0001020304050607FFFFFFFFFFFFFFFFFFFFFFFF.
        let mut creds = b"AL1000:".to_vec();
        creds.extend_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7]);
        creds.extend_from_slice(&[0xFF; 12]);
        assert_eq!(encode(&creds), "QUwxMDAwOgABAgMEBQYH////////////////");
    }
}