taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

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 }