1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
use std::{borrow::Cow, sync::LazyLock};
use base64_simd::{Base64 as Raw, Error, STANDARD};
use regex::Regex;
pub struct Base64(Raw);
impl Base64 {
pub const fn new() -> Self {
Self(STANDARD)
}
}
impl Default for Base64 {
fn default() -> Self {
Self::new()
}
}
static BASE64: Base64 = Base64::new();
pub fn encode_to_string<D: AsRef<[u8]>>(data: D) -> String {
BASE64.0.encode_to_string(data)
}
pub fn decode_to_vec<D: AsRef<[u8]>>(data: D) -> Result<Vec<u8>, Error> {
BASE64.0.decode_to_vec(data)
}
// shared regex to strip non-base64 characters before decoding
static INVALID_BASE64_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[^+/0-9A-Za-z-_]").expect("Invalid RegExp"));
// modified from https://github.com/feross/buffer/blob/795bbb5bda1b39f1370ebd784bea6107b087e3a7/index.js#L1942
// Buffer.from in nodejs will clean base64 first, which causes some inconsistent behavior with base64_simd
// e.g. Buffer.from("abcd?#iefix", "base64").toString("base64")
pub fn clean_base64(value: &str) -> Option<Cow<'_, str>> {
let value = value.split('=').next()?;
let value = value.trim();
let value = INVALID_BASE64_RE.replace_all(value, "");
if value.len() < 2 {
return Some(Cow::from(""));
}
let value = value.into_owned();
let len = value.len();
let remainder = len % 4;
if remainder == 0 {
return Some(Cow::from(value));
}
let pad_len = 4 - remainder;
if pad_len == 1 {
return Some(Cow::from(value + "="));
}
if pad_len == 2 {
return Some(Cow::from(value + "=="));
}
// modify: add this case on the original base64clean js function
// why Buffer.from("abcd?#iefix", "base64") => "abcdiefi"?
// 1. base64clean("abcd?#iefix") => "abcdiefix==="
// 2. toByteArray("abcdiefix===") => "abcdiefi"
// but base64_simd::STANDARD.decode_to_vec("abcdiefix===") will return error
// because toByteArray and base64_simd::STANDARD.decode_to_vec are different with handling placeHoldersLen
// for detail checkout:
// - https://github.com/beatgammit/base64-js/blob/88957c9943c7e2a0f03cdf73e71d579e433627d3/index.js#L80-L96
// - https://docs.rs/base64-simd/0.8.0/src/base64_simd/decode.rs.html#70 (means placeHoldersLen === 3)
Some(Cow::from(value[..len - remainder].to_owned()))
}