pub fn base64_encode(input: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(T[(n >> 18) as usize & 63] as char);
out.push(T[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
T[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
T[n as usize & 63] as char
} else {
'='
});
}
out
}
pub fn base64_decode(input: &str) -> Result<Vec<u8>, Base64Error> {
let mut acc: u32 = 0;
let mut nbits: u32 = 0;
let mut out = Vec::with_capacity(input.len() / 4 * 3);
for (i, b) in input.bytes().enumerate() {
let v = match b {
b'A'..=b'Z' => b - b'A',
b'a'..=b'z' => b - b'a' + 26,
b'0'..=b'9' => b - b'0' + 52,
b'+' | b'-' => 62,
b'/' | b'_' => 63,
b'=' => break,
b' ' | b'\n' | b'\r' | b'\t' => continue,
other => return Err(Base64Error(i, other)),
};
acc = (acc << 6) | v as u32;
nbits += 6;
if nbits >= 8 {
nbits -= 8;
out.push((acc >> nbits) as u8);
}
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Base64Error(pub usize, pub u8);
impl std::fmt::Display for Base64Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"invalid base64 byte {:#04x} at offset {}",
self.1, self.0
)
}
}
impl std::error::Error for Base64Error {}
pub fn parse_data_uri(s: &str) -> Option<(String, Vec<u8>)> {
let rest = s.strip_prefix("data:")?;
let (meta, payload) = rest.split_once(',')?;
let meta = meta.strip_suffix(";base64")?;
let media_type = if meta.is_empty() {
"application/octet-stream".to_string()
} else {
meta.to_string()
};
base64_decode(payload).ok().map(|b| (media_type, b))
}
pub mod serde_bytes_b64 {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&super::base64_encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let s = String::deserialize(d)?;
super::base64_decode(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_matches_known_vectors() {
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_encode(b"f"), "Zg==");
assert_eq!(base64_encode(b"fo"), "Zm8=");
assert_eq!(base64_encode(b"foo"), "Zm9v");
assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
}
#[test]
fn decode_matches_known_vectors() {
for s in ["", "f", "fo", "foo", "foob", "fooba", "foobar"] {
assert_eq!(
base64_decode(&base64_encode(s.as_bytes())).unwrap(),
s.as_bytes()
);
}
}
#[test]
fn decode_round_trips_all_byte_values() {
let all: Vec<u8> = (0..=255u8).collect();
assert_eq!(base64_decode(&base64_encode(&all)).unwrap(), all);
}
#[test]
fn decode_tolerates_missing_padding_and_whitespace() {
assert_eq!(base64_decode("Zm9vYmFy").unwrap(), b"foobar");
assert_eq!(base64_decode("Zg").unwrap(), b"f"); assert_eq!(base64_decode("Zm9v\nYmFy").unwrap(), b"foobar");
}
#[test]
fn decode_accepts_url_safe_alphabet() {
assert_eq!(
base64_decode("+/8=").unwrap(),
base64_decode("-_8=").unwrap()
);
}
#[test]
fn decode_rejects_stray_bytes() {
assert_eq!(base64_decode("Zm9v!YmFy"), Err(Base64Error(4, b'!')));
}
#[test]
fn data_uri_splits_media_type_and_bytes() {
let (mt, bytes) = parse_data_uri("data:image/jpeg;base64,Zm9vYmFy").unwrap();
assert_eq!(mt, "image/jpeg");
assert_eq!(bytes, b"foobar");
}
#[test]
fn data_uri_rejects_plain_urls() {
assert!(parse_data_uri("https://example.com/a.png").is_none());
assert!(parse_data_uri("data:image/png,notbase64").is_none());
}
}