const STD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
fn encode(bytes: &[u8], alphabet: &[u8; 64], pad: bool) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.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(alphabet[(n >> 18 & 63) as usize] as char);
out.push(alphabet[(n >> 12 & 63) as usize] as char);
if chunk.len() > 1 {
out.push(alphabet[(n >> 6 & 63) as usize] as char);
} else if pad {
out.push('=');
}
if chunk.len() > 2 {
out.push(alphabet[(n & 63) as usize] as char);
} else if pad {
out.push('=');
}
}
out
}
fn decode(s: &str, alphabet: &[u8; 64]) -> Option<Vec<u8>> {
let mut rev = [255u8; 256];
for (i, &c) in alphabet.iter().enumerate() {
rev[c as usize] = i as u8;
}
let chars: Vec<u8> = s
.bytes()
.filter(|&c| c != b'=' && !c.is_ascii_whitespace())
.collect();
let mut out = Vec::with_capacity(chars.len() / 4 * 3);
for chunk in chars.chunks(4) {
if chunk.len() < 2 {
return None;
}
let mut vals = [0u32; 4];
for (i, &c) in chunk.iter().enumerate() {
let v = rev[c as usize];
if v == 255 {
return None;
}
vals[i] = v as u32;
}
let n = (vals[0] << 18) | (vals[1] << 12) | (vals[2] << 6) | vals[3];
out.push((n >> 16 & 0xff) as u8);
if chunk.len() >= 3 {
out.push((n >> 8 & 0xff) as u8);
}
if chunk.len() >= 4 {
out.push((n & 0xff) as u8);
}
}
Some(out)
}
pub fn base64_encode(bytes: &[u8]) -> String {
encode(bytes, STD, true)
}
pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
decode(s, STD)
}
pub fn encode_scenario(toml: &str) -> String {
encode(toml.as_bytes(), URL, false)
}
pub fn decode_scenario(token: &str) -> Option<String> {
let bytes = decode(token, URL)?;
String::from_utf8(bytes).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc4648_standard_vectors() {
let cases = [
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
];
for (plain, b64) in cases {
assert_eq!(base64_encode(plain.as_bytes()), b64, "encode {plain}");
assert_eq!(
base64_decode(b64).unwrap(),
plain.as_bytes(),
"decode {b64}"
);
}
}
#[test]
fn url_safe_round_trips_a_scenario() {
let toml =
"kind = \"clock\"\n[time]\nstep_s = 1.0\n[gnss]\nshape = \"outage\"\n# share me 𝛂β\n";
let token = encode_scenario(toml);
assert!(!token.contains('+') && !token.contains('/') && !token.contains('='));
assert_eq!(decode_scenario(&token).as_deref(), Some(toml));
}
#[test]
fn decode_rejects_invalid_symbols() {
assert!(decode_scenario("abc+def").is_none());
assert!(base64_decode("not valid!@#").is_none());
}
#[test]
fn empty_and_binary_round_trip() {
assert_eq!(base64_encode(&[]), "");
assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
let bytes: Vec<u8> = (0..=255u8).collect();
assert_eq!(base64_decode(&base64_encode(&bytes)).unwrap(), bytes);
}
}