base64.rs (6295B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::fmt::Display; 18 19 pub const BASE64_ALPHABET: &[u8] = 20 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 21 22 /** Encoded bytes len of base64 with padding */ 23 #[inline] 24 const fn encoded_len(len: usize) -> usize { 25 (len * 4).div_ceil(3).next_multiple_of(4) 26 } 27 28 /** Encode a chunk using base64 */ 29 #[inline(always)] 30 fn encode_chunk(chunk: &[u8], encoded: &mut [u8; 4]) { 31 let mut buf = [0u8; 3]; 32 for (i, &b) in chunk.iter().enumerate() { 33 buf[i] = b; 34 } 35 encoded[0] = BASE64_ALPHABET[((buf[0] & 0xFC) >> 2) as usize]; 36 encoded[1] = BASE64_ALPHABET[(((buf[0] & 0x03) << 4) | ((buf[1] & 0xF0) >> 4)) as usize]; 37 if chunk.len() > 1 { 38 encoded[2] = BASE64_ALPHABET[(((buf[1] & 0x0F) << 2) | ((buf[2] & 0xC0) >> 6)) as usize]; 39 } 40 if chunk.len() > 2 { 41 encoded[3] = BASE64_ALPHABET[(buf[2] & 0x3F) as usize]; 42 } 43 } 44 45 /** Encode bytes using standard base64 with `=` padding */ 46 pub fn encode(bytes: impl AsRef<[u8]>) -> String { 47 let bytes = bytes.as_ref(); 48 let mut buf = vec![b'='; encoded_len(bytes.len())]; 49 50 for (chunk, buf) in bytes.chunks(3).zip(buf.as_chunks_mut().0) { 51 encode_chunk(chunk, buf) 52 } 53 54 // SAFETY: only contains ASCII characters from BASE64_ALPHABET or b'=' 55 unsafe { String::from_utf8_unchecked(buf) } 56 } 57 58 /** Format bytes using standard base64 with `=` padding */ 59 pub fn fmt(bytes: impl AsRef<[u8]>) -> impl Display { 60 std::fmt::from_fn(move |f| { 61 for chunk in bytes.as_ref().chunks(3) { 62 let mut tmp = [0u8; 3]; 63 for (i, &b) in chunk.iter().enumerate() { 64 tmp[i] = b; 65 } 66 67 let mut out = [b'='; 4]; 68 out[0] = BASE64_ALPHABET[((tmp[0] & 0xFC) >> 2) as usize]; 69 out[1] = BASE64_ALPHABET[(((tmp[0] & 0x03) << 4) | ((tmp[1] & 0xF0) >> 4)) as usize]; 70 if chunk.len() > 1 { 71 out[2] = 72 BASE64_ALPHABET[(((tmp[1] & 0x0F) << 2) | ((tmp[2] & 0xC0) >> 6)) as usize]; 73 } 74 if chunk.len() > 2 { 75 out[3] = BASE64_ALPHABET[(tmp[2] & 0x3F) as usize]; 76 } 77 78 // SAFETY: out contains only ASCII characters from BASE64_ALPHABET or b'=' 79 f.write_str(unsafe { std::str::from_utf8_unchecked(&out) })?; 80 } 81 Ok(()) 82 }) 83 } 84 85 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] 86 pub enum Base64Error { 87 #[error("invalid base64 format")] 88 Format, 89 #[error("invalid length: base64 input must be a multiple of 4")] 90 Length, 91 } 92 93 const fn build_shift_table(shift: u32) -> [u32; 256] { 94 let mut t = [0xFFFF_FFFFu32; 256]; // sentinel: invalid marker in all bits 95 let mut i = 0; 96 while i < 64 { 97 t[BASE64_ALPHABET[i] as usize] = (i as u32) << shift; 98 i += 1; 99 } 100 t 101 } 102 const T0: [u32; 256] = build_shift_table(26); 103 const T1: [u32; 256] = build_shift_table(20); 104 const T2: [u32; 256] = build_shift_table(14); 105 const T3: [u32; 256] = build_shift_table(8); 106 107 /** Decode a standard base64 string (with `=` padding) */ 108 pub fn decode(encoded: impl AsRef<[u8]>) -> Result<Vec<u8>, Base64Error> { 109 let encoded = encoded.as_ref(); 110 if encoded.len() % 4 != 0 { 111 return Err(Base64Error::Length); 112 } 113 114 // Padding, if present at all, can only be this trailing run. 115 let pad = encoded.iter().rev().take_while(|&&b| b == b'=').count(); 116 if pad > 2 { 117 return Err(Base64Error::Format); 118 } 119 120 let core = &encoded[..encoded.len() - pad]; 121 122 let out_len = core.len() / 4 * 3 123 + match core.len() % 4 { 124 2 => 1, 125 3 => 2, 126 _ => 0, 127 }; 128 let mut decoded = Vec::with_capacity(out_len); 129 let mut invalid = false; 130 131 let (chunks, tail) = core.as_chunks::<4>(); 132 for chunk in chunks { 133 let word = T0[chunk[0] as usize] 134 | T1[chunk[1] as usize] 135 | T2[chunk[2] as usize] 136 | T3[chunk[3] as usize]; 137 invalid |= word & 0xFF != 0; 138 decoded.extend_from_slice(&word.to_be_bytes()[..3]); 139 } 140 141 if !tail.is_empty() { 142 let mut word = T0[tail[0] as usize] | T1[tail[1] as usize]; 143 144 if tail.len() == 3 { 145 word |= T2[tail[2] as usize]; 146 } 147 invalid |= word & 0xFF != 0; 148 decoded.extend_from_slice(&word.to_be_bytes()[..tail.len() - 1]); 149 } 150 151 if invalid { 152 return Err(Base64Error::Format); 153 } 154 155 Ok(decoded) 156 } 157 158 #[cfg(test)] 159 mod test { 160 use crate::encoding::base64::{Base64Error, decode, encode, fmt}; 161 162 #[test] 163 fn base64() { 164 // RFC test vectors 165 for (decoded, encoded) in [ 166 ("", ""), 167 ("f", "Zg=="), 168 ("fo", "Zm8="), 169 ("foo", "Zm9v"), 170 ("foob", "Zm9vYg=="), 171 ("fooba", "Zm9vYmE="), 172 ("foobar", "Zm9vYmFy"), 173 ] { 174 assert_eq!(encode(decoded.as_bytes()), encoded); 175 assert_eq!(fmt(decoded).to_string(), encoded); 176 assert_eq!(decode(encoded.as_bytes()).unwrap(), decoded.as_bytes()); 177 } 178 179 // Invalid length 180 assert_eq!(decode(b"Zg="), Err(Base64Error::Length)); 181 assert_eq!(decode(b"Z"), Err(Base64Error::Length)); 182 183 // Invalid characters 184 assert_eq!(decode(b"Zg=!"), Err(Base64Error::Format)); 185 assert_eq!(decode(b"Z\x00=="), Err(Base64Error::Format)); 186 187 // Invalid padding 188 assert_eq!(decode(b"===="), Err(Base64Error::Format)); 189 assert_eq!(decode(b"A=BC"), Err(Base64Error::Format)); 190 assert_eq!(decode(b"AA==QUJD"), Err(Base64Error::Format)); 191 } 192 }