lodestone-proto 0.0.16

Versioned protocol envelopes and transport-neutral requests for Lodestone
Documentation
//! Canonical compact JSON representation for bounded byte vectors.

use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serializer};

const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

/// Serialize bytes as canonical unpadded URL-safe base64 for a bounded wire field.
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&encode(bytes))
}

/// Decode one canonical byte field; the enclosing protocol must bound its input.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
    D: Deserializer<'de>,
{
    let encoded = String::deserialize(deserializer)?;
    decode(encoded.as_bytes()).map_err(D::Error::custom)
}

fn encode(bytes: &[u8]) -> String {
    let tail = match bytes.len() % 3 {
        0 => 0,
        1 => 2,
        _ => 3,
    };
    let capacity = bytes.len() / 3 * 4 + tail;
    let mut encoded = String::with_capacity(capacity);
    for chunk in bytes.chunks(3) {
        let first = chunk[0];
        let second = chunk.get(1).copied().unwrap_or(0);
        let third = chunk.get(2).copied().unwrap_or(0);
        encoded.push(char::from(ALPHABET[usize::from(first >> 2)]));
        encoded.push(char::from(ALPHABET[usize::from(((first & 0x03) << 4) | (second >> 4))]));
        if chunk.len() > 1 {
            encoded.push(char::from(ALPHABET[usize::from(((second & 0x0f) << 2) | (third >> 6))]));
        }
        if chunk.len() > 2 {
            encoded.push(char::from(ALPHABET[usize::from(third & 0x3f)]));
        }
    }
    encoded
}

fn decode(encoded: &[u8]) -> Result<Vec<u8>, &'static str> {
    if encoded.len() % 4 == 1 {
        return Err("invalid base64 length");
    }
    let capacity = encoded.len().checked_mul(3).ok_or("base64 length overflow")? / 4;
    let mut decoded = Vec::with_capacity(capacity);
    let (chunks, remainder) = encoded.as_chunks::<4>();
    for chunk in chunks {
        let first = value(chunk[0])?;
        let second = value(chunk[1])?;
        let third = value(chunk[2])?;
        let fourth = value(chunk[3])?;
        decoded.push((first << 2) | (second >> 4));
        decoded.push((second << 4) | (third >> 2));
        decoded.push((third << 6) | fourth);
    }
    match remainder {
        [] => {}
        [first, second] => {
            let first = value(*first)?;
            let second = value(*second)?;
            if second & 0x0f != 0 {
                return Err("non-canonical base64 tail");
            }
            decoded.push((first << 2) | (second >> 4));
        }
        [first, second, third] => {
            let first = value(*first)?;
            let second = value(*second)?;
            let third = value(*third)?;
            if third & 0x03 != 0 {
                return Err("non-canonical base64 tail");
            }
            decoded.push((first << 2) | (second >> 4));
            decoded.push((second << 4) | (third >> 2));
        }
        _ => return Err("invalid base64 tail"),
    }
    Ok(decoded)
}

fn value(encoded: u8) -> Result<u8, &'static str> {
    match encoded {
        b'A'..=b'Z' => Ok(encoded - b'A'),
        b'a'..=b'z' => Ok(encoded - b'a' + 26),
        b'0'..=b'9' => Ok(encoded - b'0' + 52),
        b'-' => Ok(62),
        b'_' => Ok(63),
        _ => Err("invalid base64 character"),
    }
}