Skip to main content

use_base64/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Base64Variant {
6    Standard,
7    UrlSafe,
8    StandardNoPadding,
9    UrlSafeNoPadding,
10}
11
12#[must_use]
13pub fn base64_encode(input: &[u8]) -> String {
14    encode_base64(input, Base64Variant::Standard)
15}
16
17pub fn base64_decode(input: &str) -> Option<Vec<u8>> {
18    decode_base64(input, false)
19}
20
21#[must_use]
22pub fn base64_url_encode(input: &[u8]) -> String {
23    encode_base64(input, Base64Variant::UrlSafe)
24}
25
26pub fn base64_url_decode(input: &str) -> Option<Vec<u8>> {
27    decode_base64(input, true)
28}
29
30#[must_use]
31pub fn looks_like_base64(input: &str) -> bool {
32    normalize_base64_input(input).is_some()
33        && input
34            .trim_end_matches('=')
35            .chars()
36            .all(|c| is_base64_char(c) || is_base64_url_char(c))
37}
38
39#[must_use]
40pub fn is_base64_char(c: char) -> bool {
41    c.is_ascii_alphanumeric() || matches!(c, '+' | '/')
42}
43
44#[must_use]
45pub fn is_base64_url_char(c: char) -> bool {
46    c.is_ascii_alphanumeric() || matches!(c, '-' | '_')
47}
48
49#[must_use]
50pub fn base64_padding_len(input: &str) -> usize {
51    input.chars().rev().take_while(|&c| c == '=').count()
52}
53
54#[must_use]
55pub fn normalize_base64_padding(input: &str) -> String {
56    if input.is_empty() {
57        return String::new();
58    }
59
60    if let Some(position) = input.find('=') {
61        let suffix = &input[position..];
62        if suffix.chars().all(|c| c == '=') && input.len().is_multiple_of(4) && suffix.len() <= 2 {
63            return input.to_string();
64        }
65
66        return input.to_string();
67    }
68
69    match input.len() % 4 {
70        0 => input.to_string(),
71        2 => format!("{input}=="),
72        3 => format!("{input}="),
73        _ => input.to_string(),
74    }
75}
76
77fn encode_base64(input: &[u8], variant: Base64Variant) -> String {
78    let (alphabet, padding) = match variant {
79        Base64Variant::Standard => (STANDARD_ALPHABET, true),
80        Base64Variant::UrlSafe => (URL_SAFE_ALPHABET, true),
81        Base64Variant::StandardNoPadding => (STANDARD_ALPHABET, false),
82        Base64Variant::UrlSafeNoPadding => (URL_SAFE_ALPHABET, false),
83    };
84    let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
85
86    for chunk in input.chunks(3) {
87        let first = chunk[0];
88        let second = chunk.get(1).copied().unwrap_or(0);
89        let third = chunk.get(2).copied().unwrap_or(0);
90        let combined = (u32::from(first) << 16) | (u32::from(second) << 8) | u32::from(third);
91
92        output.push(alphabet[((combined >> 18) & 0x3f) as usize] as char);
93        output.push(alphabet[((combined >> 12) & 0x3f) as usize] as char);
94
95        if chunk.len() > 1 {
96            output.push(alphabet[((combined >> 6) & 0x3f) as usize] as char);
97        } else if padding {
98            output.push('=');
99        }
100
101        if chunk.len() > 2 {
102            output.push(alphabet[(combined & 0x3f) as usize] as char);
103        } else if padding {
104            output.push('=');
105        }
106    }
107
108    output
109}
110
111fn decode_base64(input: &str, url_safe: bool) -> Option<Vec<u8>> {
112    let normalized = normalize_base64_input(input)?;
113    let bytes = normalized.as_bytes();
114    let chunk_count = bytes.len() / 4;
115    let mut output = Vec::with_capacity((bytes.len() / 4) * 3);
116
117    for (chunk_index, chunk) in bytes.chunks(4).enumerate() {
118        let last_chunk = chunk_index + 1 == chunk_count;
119        let third_is_padding = chunk[2] == b'=';
120        let fourth_is_padding = chunk[3] == b'=';
121
122        if third_is_padding && !fourth_is_padding {
123            return None;
124        }
125
126        if (third_is_padding || fourth_is_padding) && !last_chunk {
127            return None;
128        }
129
130        let first = u32::from(decode_base64_byte(chunk[0], url_safe)?);
131        let second = u32::from(decode_base64_byte(chunk[1], url_safe)?);
132        let third = if third_is_padding {
133            0
134        } else {
135            u32::from(decode_base64_byte(chunk[2], url_safe)?)
136        };
137        let fourth = if fourth_is_padding {
138            0
139        } else {
140            u32::from(decode_base64_byte(chunk[3], url_safe)?)
141        };
142
143        let combined = (first << 18) | (second << 12) | (third << 6) | fourth;
144        output.push(((combined >> 16) & 0xff) as u8);
145        if !third_is_padding {
146            output.push(((combined >> 8) & 0xff) as u8);
147        }
148        if !fourth_is_padding {
149            output.push((combined & 0xff) as u8);
150        }
151    }
152
153    Some(output)
154}
155
156fn normalize_base64_input(input: &str) -> Option<String> {
157    if !input.is_ascii() {
158        return None;
159    }
160
161    if input.is_empty() {
162        return Some(String::new());
163    }
164
165    if let Some(position) = input.find('=') {
166        let suffix = &input[position..];
167        if !input.len().is_multiple_of(4) || suffix.len() > 2 || !suffix.chars().all(|c| c == '=') {
168            return None;
169        }
170
171        return Some(input.to_string());
172    }
173
174    match input.len() % 4 {
175        0 => Some(input.to_string()),
176        2 => Some(format!("{input}==")),
177        3 => Some(format!("{input}=")),
178        _ => None,
179    }
180}
181
182fn decode_base64_byte(byte: u8, url_safe: bool) -> Option<u8> {
183    match byte {
184        b'A'..=b'Z' => Some(byte - b'A'),
185        b'a'..=b'z' => Some(byte - b'a' + 26),
186        b'0'..=b'9' => Some(byte - b'0' + 52),
187        b'+' if !url_safe => Some(62),
188        b'/' if !url_safe => Some(63),
189        b'-' if url_safe => Some(62),
190        b'_' if url_safe => Some(63),
191        _ => None,
192    }
193}
194
195const STANDARD_ALPHABET: &[u8; 64] =
196    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
197const URL_SAFE_ALPHABET: &[u8; 64] =
198    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";