Skip to main content

use_base32/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Base32Variant {
6    Standard,
7    NoPadding,
8}
9
10#[must_use]
11pub fn base32_encode(input: &[u8]) -> String {
12    encode_base32(input, Base32Variant::Standard)
13}
14
15pub fn base32_decode(input: &str) -> Option<Vec<u8>> {
16    let normalized = normalize_base32_input(input)?;
17    let trimmed = normalized.trim_end_matches('=');
18    let mut output = Vec::with_capacity((trimmed.len() * 5) / 8);
19    let mut buffer = 0_u32;
20    let mut bits = 0_u8;
21
22    for character in trimmed.chars() {
23        let value = u32::from(decode_base32_char(character)?);
24        buffer = (buffer << 5) | value;
25        bits += 5;
26
27        while bits >= 8 {
28            bits -= 8;
29            output.push(((buffer >> bits) & 0xff) as u8);
30        }
31    }
32
33    if bits > 0 && (buffer & ((1_u32 << bits) - 1)) != 0 {
34        return None;
35    }
36
37    Some(output)
38}
39
40#[must_use]
41pub fn looks_like_base32(input: &str) -> bool {
42    normalize_base32_input(input).is_some()
43        && input.trim_end_matches('=').chars().all(is_base32_char)
44}
45
46#[must_use]
47pub fn is_base32_char(c: char) -> bool {
48    matches!(c, 'A'..='Z' | 'a'..='z' | '2'..='7')
49}
50
51#[must_use]
52pub fn base32_padding_len(input: &str) -> usize {
53    input.chars().rev().take_while(|&c| c == '=').count()
54}
55
56#[must_use]
57pub fn normalize_base32_padding(input: &str) -> String {
58    if input.is_empty() {
59        return String::new();
60    }
61
62    if let Some(position) = input.find('=') {
63        let suffix = &input[position..];
64        if input.len().is_multiple_of(8)
65            && matches!(suffix.len(), 1 | 3 | 4 | 6)
66            && suffix.chars().all(|c| c == '=')
67        {
68            return input.to_string();
69        }
70
71        return input.to_string();
72    }
73
74    match input.len() % 8 {
75        0 => input.to_string(),
76        2 => format!("{input}======"),
77        4 => format!("{input}===="),
78        5 => format!("{input}==="),
79        7 => format!("{input}="),
80        _ => input.to_string(),
81    }
82}
83
84fn encode_base32(input: &[u8], variant: Base32Variant) -> String {
85    let mut output = String::with_capacity(((input.len() * 8).div_ceil(5)).max(1));
86    let mut buffer = 0_u32;
87    let mut bits = 0_u8;
88
89    for byte in input {
90        buffer = (buffer << 8) | u32::from(*byte);
91        bits += 8;
92
93        while bits >= 5 {
94            bits -= 5;
95            output.push(BASE32_ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
96        }
97    }
98
99    if bits > 0 {
100        output.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
101    }
102
103    if matches!(variant, Base32Variant::Standard) {
104        while !output.len().is_multiple_of(8) {
105            output.push('=');
106        }
107    }
108
109    output
110}
111
112fn normalize_base32_input(input: &str) -> Option<String> {
113    if !input.is_ascii() {
114        return None;
115    }
116
117    if input.is_empty() {
118        return Some(String::new());
119    }
120
121    if let Some(position) = input.find('=') {
122        let suffix = &input[position..];
123        if !input.len().is_multiple_of(8)
124            || !matches!(suffix.len(), 1 | 3 | 4 | 6)
125            || !suffix.chars().all(|c| c == '=')
126        {
127            return None;
128        }
129
130        return Some(input.to_string());
131    }
132
133    match input.len() % 8 {
134        0 => Some(input.to_string()),
135        2 => Some(format!("{input}======")),
136        4 => Some(format!("{input}====")),
137        5 => Some(format!("{input}===")),
138        7 => Some(format!("{input}=")),
139        _ => None,
140    }
141}
142
143fn decode_base32_char(character: char) -> Option<u8> {
144    match character {
145        'A'..='Z' => Some(character as u8 - b'A'),
146        'a'..='z' => Some(character as u8 - b'a'),
147        '2'..='7' => Some(character as u8 - b'2' + 26),
148        _ => None,
149    }
150}
151
152const BASE32_ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";