1const STD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15const URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
16
17fn encode(bytes: &[u8], alphabet: &[u8; 64], pad: bool) -> String {
20 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
21 for chunk in bytes.chunks(3) {
22 let b0 = chunk[0] as u32;
23 let b1 = *chunk.get(1).unwrap_or(&0) as u32;
24 let b2 = *chunk.get(2).unwrap_or(&0) as u32;
25 let n = (b0 << 16) | (b1 << 8) | b2;
26 out.push(alphabet[(n >> 18 & 63) as usize] as char);
27 out.push(alphabet[(n >> 12 & 63) as usize] as char);
28 if chunk.len() > 1 {
29 out.push(alphabet[(n >> 6 & 63) as usize] as char);
30 } else if pad {
31 out.push('=');
32 }
33 if chunk.len() > 2 {
34 out.push(alphabet[(n & 63) as usize] as char);
35 } else if pad {
36 out.push('=');
37 }
38 }
39 out
40}
41
42fn decode(s: &str, alphabet: &[u8; 64]) -> Option<Vec<u8>> {
46 let mut rev = [255u8; 256];
47 for (i, &c) in alphabet.iter().enumerate() {
48 rev[c as usize] = i as u8;
49 }
50 let chars: Vec<u8> = s
51 .bytes()
52 .filter(|&c| c != b'=' && !c.is_ascii_whitespace())
53 .collect();
54 let mut out = Vec::with_capacity(chars.len() / 4 * 3);
55 for chunk in chars.chunks(4) {
56 if chunk.len() < 2 {
57 return None;
58 }
59 let mut vals = [0u32; 4];
60 for (i, &c) in chunk.iter().enumerate() {
61 let v = rev[c as usize];
62 if v == 255 {
63 return None;
64 }
65 vals[i] = v as u32;
66 }
67 let n = (vals[0] << 18) | (vals[1] << 12) | (vals[2] << 6) | vals[3];
68 out.push((n >> 16 & 0xff) as u8);
69 if chunk.len() >= 3 {
70 out.push((n >> 8 & 0xff) as u8);
71 }
72 if chunk.len() >= 4 {
73 out.push((n & 0xff) as u8);
74 }
75 }
76 Some(out)
77}
78
79pub fn base64_encode(bytes: &[u8]) -> String {
81 encode(bytes, STD, true)
82}
83
84pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
86 decode(s, STD)
87}
88
89pub fn encode_scenario(toml: &str) -> String {
92 encode(toml.as_bytes(), URL, false)
93}
94
95pub fn decode_scenario(token: &str) -> Option<String> {
98 let bytes = decode(token, URL)?;
99 String::from_utf8(bytes).ok()
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn rfc4648_standard_vectors() {
108 let cases = [
110 ("", ""),
111 ("f", "Zg=="),
112 ("fo", "Zm8="),
113 ("foo", "Zm9v"),
114 ("foob", "Zm9vYg=="),
115 ("fooba", "Zm9vYmE="),
116 ("foobar", "Zm9vYmFy"),
117 ];
118 for (plain, b64) in cases {
119 assert_eq!(base64_encode(plain.as_bytes()), b64, "encode {plain}");
120 assert_eq!(
121 base64_decode(b64).unwrap(),
122 plain.as_bytes(),
123 "decode {b64}"
124 );
125 }
126 }
127
128 #[test]
129 fn url_safe_round_trips_a_scenario() {
130 let toml =
131 "kind = \"clock\"\n[time]\nstep_s = 1.0\n[gnss]\nshape = \"outage\"\n# share me 𝛂β\n";
132 let token = encode_scenario(toml);
133 assert!(!token.contains('+') && !token.contains('/') && !token.contains('='));
135 assert_eq!(decode_scenario(&token).as_deref(), Some(toml));
136 }
137
138 #[test]
139 fn decode_rejects_invalid_symbols() {
140 assert!(decode_scenario("abc+def").is_none());
142 assert!(base64_decode("not valid!@#").is_none());
144 }
145
146 #[test]
147 fn empty_and_binary_round_trip() {
148 assert_eq!(base64_encode(&[]), "");
149 assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
150 let bytes: Vec<u8> = (0..=255u8).collect();
152 assert_eq!(base64_decode(&base64_encode(&bytes)).unwrap(), bytes);
153 }
154}