modo/encoding/
base64url.rs1const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
13
14pub fn encode(bytes: &[u8]) -> String {
29 if bytes.is_empty() {
30 return String::new();
31 }
32 let mut result = String::with_capacity((bytes.len() * 4).div_ceil(3));
33 let mut buffer: u32 = 0;
34 let mut bits_left = 0;
35
36 for &byte in bytes {
37 buffer = (buffer << 8) | byte as u32;
38 bits_left += 8;
39 while bits_left >= 6 {
40 bits_left -= 6;
41 let idx = ((buffer >> bits_left) & 0x3F) as usize;
42 result.push(ALPHABET[idx] as char);
43 }
44 }
45 if bits_left > 0 {
46 let idx = ((buffer << (6 - bits_left)) & 0x3F) as usize;
47 result.push(ALPHABET[idx] as char);
48 }
49 result
50}
51
52pub fn decode(encoded: &str) -> crate::Result<Vec<u8>> {
72 if encoded.is_empty() {
73 return Ok(Vec::new());
74 }
75 let mut result = Vec::with_capacity(encoded.len() * 3 / 4);
76 let mut buffer: u32 = 0;
77 let mut bits_left = 0;
78
79 for ch in encoded.chars() {
80 let val = decode_char(ch)?;
81 buffer = (buffer << 6) | val as u32;
82 bits_left += 6;
83 if bits_left >= 8 {
84 bits_left -= 8;
85 result.push((buffer >> bits_left) as u8);
86 }
87 }
88 Ok(result)
89}
90
91fn decode_char(ch: char) -> crate::Result<u8> {
92 match ch {
93 'A'..='Z' => Ok(ch as u8 - b'A'),
94 'a'..='z' => Ok(ch as u8 - b'a' + 26),
95 '0'..='9' => Ok(ch as u8 - b'0' + 52),
96 '-' => Ok(62),
97 '_' => Ok(63),
98 _ => Err(crate::Error::bad_request(format!(
99 "invalid base64url character: '{ch}'"
100 ))),
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn encode_empty() {
110 assert_eq!(encode(b""), "");
111 }
112
113 #[test]
114 fn encode_basic() {
115 assert_eq!(encode(b"Hello"), "SGVsbG8");
117 }
118
119 #[test]
120 fn encode_uses_url_safe_chars() {
121 let bytes = [0xfb, 0xff, 0xfe];
123 let encoded = encode(&bytes);
124 assert!(!encoded.contains('+'), "should use - not +");
125 assert!(!encoded.contains('/'), "should use _ not /");
126 assert!(encoded.contains('-') || encoded.contains('_'));
127 }
128
129 #[test]
130 fn decode_basic() {
131 assert_eq!(decode("SGVsbG8").unwrap(), b"Hello");
132 }
133
134 #[test]
135 fn roundtrip_random_bytes() {
136 let bytes: Vec<u8> = (0..=255).collect();
137 let encoded = encode(&bytes);
138 let decoded = decode(&encoded).unwrap();
139 assert_eq!(decoded, bytes);
140 }
141
142 #[test]
143 fn decode_invalid_char() {
144 assert!(decode("SGVs!G8").is_err());
145 }
146
147 #[test]
148 fn encode_32_bytes_pkce() {
149 let bytes = [0xABu8; 32];
150 let encoded = encode(&bytes);
151 let decoded = decode(&encoded).unwrap();
152 assert_eq!(decoded, bytes);
153 }
154}