1const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5
6pub fn base64url_encode(b: &[u8]) -> String {
8 let mut s = String::with_capacity(b.len().div_ceil(3) * 4);
9 for c in b.chunks(3) {
10 let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
11 for i in 0..=c.len() {
12 s.push(ALPHABET[(n >> (18 - 6 * i) & 63) as usize] as char);
13 }
14 }
15 s
16}
17
18pub fn base64url_decode(s: &[u8]) -> Option<Vec<u8>> {
21 let s = s.trim_ascii();
22 let s = match s.iter().position(|&c| c == b'=') {
23 Some(p) if s[p..].iter().all(|&c| c == b'=') && s[p..].len() <= 2 => &s[..p],
24 Some(_) => return None,
25 None => s,
26 };
27 if s.len() % 4 == 1 {
28 return None;
29 }
30 let mut out = Vec::with_capacity(s.len() * 3 / 4);
31 let mut acc = 0u32;
32 let mut bits = 0;
33 for &c in s {
34 let v = match c {
35 b'A'..=b'Z' => c - b'A',
36 b'a'..=b'z' => c - b'a' + 26,
37 b'0'..=b'9' => c - b'0' + 52,
38 b'-' | b'+' => 62,
39 b'_' | b'/' => 63,
40 _ => return None,
41 };
42 acc = acc << 6 | v as u32;
43 bits += 6;
44 if bits >= 8 {
45 bits -= 8;
46 out.push((acc >> bits) as u8);
47 }
48 }
49 Some(out)
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn round_trip() {
58 for n in 0..40usize {
59 let b: Vec<u8> = (0..n).map(|i| (i * 89 + 7) as u8).collect();
60 let e = base64url_encode(&b);
61 assert!(!e.contains('=') && !e.contains('+') && !e.contains('/'));
62 assert_eq!(base64url_decode(e.as_bytes()).unwrap(), b);
63 }
64 assert_eq!(base64url_encode(b"\xfb\xff"), "-_8");
65 assert_eq!(base64url_decode(b"AAMAAABkAAQAAP__").unwrap(), [0, 3, 0, 0, 0, 100, 0, 4, 0, 0, 255, 255]);
66 assert_eq!(base64url_decode(b" QQ== ").unwrap(), b"A");
67 assert!(base64url_decode(b"Q").is_none());
68 assert!(base64url_decode(b"Q=Q=").is_none());
69 assert!(base64url_decode(b"QQ!!").is_none());
70 }
71}