Skip to main content

oxihuman_core/
base64_codec.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Base64 encode/decode (standard alphabet, no external deps).
5
6#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct Base64Config {
9    pub url_safe: bool,
10}
11
12#[allow(dead_code)]
13pub fn default_base64_config() -> Base64Config {
14    Base64Config { url_safe: false }
15}
16
17const STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
18const URL_SAFE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
19
20#[allow(dead_code)]
21fn alphabet(url_safe: bool) -> &'static [u8; 64] {
22    if url_safe {
23        URL_SAFE
24    } else {
25        STANDARD
26    }
27}
28
29#[allow(dead_code)]
30pub fn base64_encode(bytes: &[u8]) -> String {
31    base64_encode_with_config(bytes, false)
32}
33
34#[allow(dead_code)]
35fn base64_encode_with_config(bytes: &[u8], url_safe: bool) -> String {
36    let table = alphabet(url_safe);
37    let mut out = Vec::with_capacity(base64_encoded_len(bytes.len()));
38    let mut i = 0;
39    while i + 3 <= bytes.len() {
40        let b0 = bytes[i] as u32;
41        let b1 = bytes[i + 1] as u32;
42        let b2 = bytes[i + 2] as u32;
43        out.push(table[((b0 >> 2) & 0x3f) as usize]);
44        out.push(table[((b0 << 4 | b1 >> 4) & 0x3f) as usize]);
45        out.push(table[((b1 << 2 | b2 >> 6) & 0x3f) as usize]);
46        out.push(table[(b2 & 0x3f) as usize]);
47        i += 3;
48    }
49    let rem = bytes.len() - i;
50    if rem == 1 {
51        let b0 = bytes[i] as u32;
52        out.push(table[((b0 >> 2) & 0x3f) as usize]);
53        out.push(table[((b0 << 4) & 0x3f) as usize]);
54        out.push(b'=');
55        out.push(b'=');
56    } else if rem == 2 {
57        let b0 = bytes[i] as u32;
58        let b1 = bytes[i + 1] as u32;
59        out.push(table[((b0 >> 2) & 0x3f) as usize]);
60        out.push(table[((b0 << 4 | b1 >> 4) & 0x3f) as usize]);
61        out.push(table[((b1 << 2) & 0x3f) as usize]);
62        out.push(b'=');
63    }
64    // SAFETY: all bytes are valid ASCII
65    unsafe { String::from_utf8_unchecked(out) }
66}
67
68#[allow(dead_code)]
69pub fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
70    let s = s.trim_end_matches('=');
71    let mut out = Vec::with_capacity(base64_decoded_len(s.len() + (4 - s.len() % 4) % 4));
72    let table: [i8; 256] = {
73        let mut t = [-1i8; 256];
74        for (i, &b) in STANDARD.iter().enumerate() {
75            t[b as usize] = i as i8;
76        }
77        // Also support url-safe chars
78        t[b'-' as usize] = 62;
79        t[b'_' as usize] = 63;
80        t
81    };
82    let bytes = s.as_bytes();
83    let mut i = 0;
84    while i + 4 <= bytes.len() {
85        let v: Vec<i8> = (0..4).map(|j| table[bytes[i + j] as usize]).collect();
86        if v.iter().any(|&x| x < 0) {
87            return Err("invalid base64 character".to_string());
88        }
89        let (c0, c1, c2, c3) = (v[0] as u8, v[1] as u8, v[2] as u8, v[3] as u8);
90        out.push((c0 << 2) | (c1 >> 4));
91        out.push((c1 << 4) | (c2 >> 2));
92        out.push((c2 << 6) | c3);
93        i += 4;
94    }
95    let rem = bytes.len() - i;
96    if rem == 2 {
97        let (c0, c1) = (table[bytes[i] as usize], table[bytes[i + 1] as usize]);
98        if c0 < 0 || c1 < 0 {
99            return Err("invalid base64 character".to_string());
100        }
101        out.push(((c0 as u8) << 2) | ((c1 as u8) >> 4));
102    } else if rem == 3 {
103        let (c0, c1, c2) = (
104            table[bytes[i] as usize],
105            table[bytes[i + 1] as usize],
106            table[bytes[i + 2] as usize],
107        );
108        if c0 < 0 || c1 < 0 || c2 < 0 {
109            return Err("invalid base64 character".to_string());
110        }
111        out.push(((c0 as u8) << 2) | ((c1 as u8) >> 4));
112        out.push(((c1 as u8) << 4) | ((c2 as u8) >> 2));
113    } else if rem == 1 {
114        return Err("invalid base64 length".to_string());
115    }
116    Ok(out)
117}
118
119#[allow(dead_code)]
120pub fn base64_encode_str(s: &str) -> String {
121    base64_encode(s.as_bytes())
122}
123
124#[allow(dead_code)]
125pub fn base64_is_valid(s: &str) -> bool {
126    base64_decode(s).is_ok()
127}
128
129#[allow(dead_code)]
130pub fn base64_encoded_len(byte_len: usize) -> usize {
131    byte_len.div_ceil(3) * 4
132}
133
134#[allow(dead_code)]
135pub fn base64_decoded_len(encoded_len: usize) -> usize {
136    (encoded_len / 4) * 3
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_default_config() {
145        let cfg = default_base64_config();
146        assert!(!cfg.url_safe);
147    }
148
149    #[test]
150    fn test_encode_empty() {
151        assert_eq!(base64_encode(b""), "");
152    }
153
154    #[test]
155    fn test_encode_hello() {
156        assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
157    }
158
159    #[test]
160    fn test_roundtrip_ascii() {
161        let original = b"Hello, World!";
162        let encoded = base64_encode(original);
163        let decoded = base64_decode(&encoded).expect("should succeed");
164        assert_eq!(decoded, original);
165    }
166
167    #[test]
168    fn test_roundtrip_binary() {
169        let original: Vec<u8> = (0u8..=255).collect();
170        let encoded = base64_encode(&original);
171        let decoded = base64_decode(&encoded).expect("should succeed");
172        assert_eq!(decoded, original);
173    }
174
175    #[test]
176    fn test_encode_str() {
177        let s = "test";
178        let encoded = base64_encode_str(s);
179        let decoded = base64_decode(&encoded).expect("should succeed");
180        assert_eq!(decoded, s.as_bytes());
181    }
182
183    #[test]
184    fn test_is_valid() {
185        assert!(base64_is_valid("aGVsbG8="));
186        assert!(!base64_is_valid("!!!invalid!!!"));
187    }
188
189    #[test]
190    fn test_encoded_len() {
191        assert_eq!(base64_encoded_len(3), 4);
192        assert_eq!(base64_encoded_len(4), 8);
193    }
194
195    #[test]
196    fn test_decoded_len() {
197        assert_eq!(base64_decoded_len(4), 3);
198        assert_eq!(base64_decoded_len(8), 6);
199    }
200}