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";