use-base32 0.1.0

Practical Base32 helpers for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Base32Variant {
    Standard,
    NoPadding,
}

#[must_use]
pub fn base32_encode(input: &[u8]) -> String {
    encode_base32(input, Base32Variant::Standard)
}

pub fn base32_decode(input: &str) -> Option<Vec<u8>> {
    let normalized = normalize_base32_input(input)?;
    let trimmed = normalized.trim_end_matches('=');
    let mut output = Vec::with_capacity((trimmed.len() * 5) / 8);
    let mut buffer = 0_u32;
    let mut bits = 0_u8;

    for character in trimmed.chars() {
        let value = u32::from(decode_base32_char(character)?);
        buffer = (buffer << 5) | value;
        bits += 5;

        while bits >= 8 {
            bits -= 8;
            output.push(((buffer >> bits) & 0xff) as u8);
        }
    }

    if bits > 0 && (buffer & ((1_u32 << bits) - 1)) != 0 {
        return None;
    }

    Some(output)
}

#[must_use]
pub fn looks_like_base32(input: &str) -> bool {
    normalize_base32_input(input).is_some()
        && input.trim_end_matches('=').chars().all(is_base32_char)
}

#[must_use]
pub fn is_base32_char(c: char) -> bool {
    matches!(c, 'A'..='Z' | 'a'..='z' | '2'..='7')
}

#[must_use]
pub fn base32_padding_len(input: &str) -> usize {
    input.chars().rev().take_while(|&c| c == '=').count()
}

#[must_use]
pub fn normalize_base32_padding(input: &str) -> String {
    if input.is_empty() {
        return String::new();
    }

    if let Some(position) = input.find('=') {
        let suffix = &input[position..];
        if input.len().is_multiple_of(8)
            && matches!(suffix.len(), 1 | 3 | 4 | 6)
            && suffix.chars().all(|c| c == '=')
        {
            return input.to_string();
        }

        return input.to_string();
    }

    match input.len() % 8 {
        0 => input.to_string(),
        2 => format!("{input}======"),
        4 => format!("{input}===="),
        5 => format!("{input}==="),
        7 => format!("{input}="),
        _ => input.to_string(),
    }
}

fn encode_base32(input: &[u8], variant: Base32Variant) -> String {
    let mut output = String::with_capacity(((input.len() * 8).div_ceil(5)).max(1));
    let mut buffer = 0_u32;
    let mut bits = 0_u8;

    for byte in input {
        buffer = (buffer << 8) | u32::from(*byte);
        bits += 8;

        while bits >= 5 {
            bits -= 5;
            output.push(BASE32_ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
        }
    }

    if bits > 0 {
        output.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
    }

    if matches!(variant, Base32Variant::Standard) {
        while !output.len().is_multiple_of(8) {
            output.push('=');
        }
    }

    output
}

fn normalize_base32_input(input: &str) -> Option<String> {
    if !input.is_ascii() {
        return None;
    }

    if input.is_empty() {
        return Some(String::new());
    }

    if let Some(position) = input.find('=') {
        let suffix = &input[position..];
        if !input.len().is_multiple_of(8)
            || !matches!(suffix.len(), 1 | 3 | 4 | 6)
            || !suffix.chars().all(|c| c == '=')
        {
            return None;
        }

        return Some(input.to_string());
    }

    match input.len() % 8 {
        0 => Some(input.to_string()),
        2 => Some(format!("{input}======")),
        4 => Some(format!("{input}====")),
        5 => Some(format!("{input}===")),
        7 => Some(format!("{input}=")),
        _ => None,
    }
}

fn decode_base32_char(character: char) -> Option<u8> {
    match character {
        'A'..='Z' => Some(character as u8 - b'A'),
        'a'..='z' => Some(character as u8 - b'a'),
        '2'..='7' => Some(character as u8 - b'2' + 26),
        _ => None,
    }
}

const BASE32_ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";