firebase_rs_sdk/util/
base64.rs

1use base64::engine::general_purpose::URL_SAFE;
2use base64::engine::Engine as _;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct DecodeBase64Error;
7
8impl fmt::Display for DecodeBase64Error {
9    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10        write!(f, "failed to decode base64 string")
11    }
12}
13
14impl std::error::Error for DecodeBase64Error {}
15
16/// Encode a string using a URL-safe base64 alphabet with `.` padding to match the JS SDK.
17pub fn base64_encode(input: &str) -> String {
18    base64_url_encode(input)
19}
20
21/// Encode a string using the JS SDK web-safe alphabet.
22pub fn base64_url_encode(input: &str) -> String {
23    base64_url_encode_bytes(input.as_bytes())
24}
25
26/// Encode a byte slice using the JS SDK web-safe alphabet (padding replaced with `.`).
27pub fn base64_url_encode_bytes(bytes: &[u8]) -> String {
28    let encoded = URL_SAFE.encode(bytes);
29    encoded.replace('=', ".")
30}
31
32/// Encode and keep the JS SDK behaviour of trimming trailing padding characters.
33pub fn base64_url_encode_trimmed(input: &str) -> String {
34    base64_encode(input).trim_end_matches('.').to_owned()
35}
36
37/// Decode a base64 URL-safe string, returning UTF-8 text on success.
38pub fn base64_decode(input: &str) -> Result<String, DecodeBase64Error> {
39    let bytes = base64_decode_bytes(input)?;
40    String::from_utf8(bytes).map_err(|_err| DecodeBase64Error)
41}
42
43/// Decode into raw bytes.
44pub fn base64_decode_bytes(input: &str) -> Result<Vec<u8>, DecodeBase64Error> {
45    let mut normalized = input.replace('.', "=");
46    // The JS SDK allows the padding to be absent. Restore padding so the decoder accepts it.
47    let remainder = normalized.len() % 4;
48    if remainder != 0 {
49        normalized.extend("====".chars().take(4 - remainder));
50    }
51    URL_SAFE
52        .decode(normalized.as_bytes())
53        .map_err(|_err| DecodeBase64Error)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn encode_and_decode_roundtrip() {
62        let original = "hello firebase";
63        let encoded = base64_encode(original);
64        let decoded = base64_decode(&encoded).unwrap();
65        assert_eq!(decoded, original);
66    }
67
68    #[test]
69    fn encode_trimmed_removes_padding() {
70        let trimmed = base64_url_encode_trimmed("test");
71        assert!(!trimmed.ends_with('.'));
72    }
73
74    #[test]
75    fn decode_tolerates_missing_padding() {
76        let encoded = base64_encode("data");
77        let without_padding = encoded.trim_end_matches('.');
78        let decoded = base64_decode(without_padding).unwrap();
79        assert_eq!(decoded, "data");
80    }
81
82    #[test]
83    fn decode_invalid_returns_error() {
84        assert!(base64_decode("@@invalid@@").is_err());
85    }
86}