Skip to main content

tera_contrib/
base64.rs

1use base64::{Engine, engine::general_purpose};
2use tera::{Kwargs, State, TeraResult};
3
4const STANDARD_DECODE: general_purpose::GeneralPurpose = general_purpose::GeneralPurpose::new(
5    &base64::alphabet::STANDARD,
6    general_purpose::GeneralPurposeConfig::new()
7        .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
8);
9
10const URL_SAFE_DECODE: general_purpose::GeneralPurpose = general_purpose::GeneralPurpose::new(
11    &base64::alphabet::URL_SAFE,
12    general_purpose::GeneralPurposeConfig::new()
13        .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
14);
15
16/// Encodes a string to base64.
17/// Takes an optional `url_safe` bool parameter (`false` by default) if you want to use the URL SAFE b64 characters
18/// and a `padded` bool parameter on whether you want padding (`true` by default).
19///
20/// ```text
21/// {{ value | b64_encode }}
22/// {{ value | b64_encode(url_safe=true) }}
23/// {{ value | b64_encode(url_safe=true, padded=false) }}
24/// ```
25pub fn b64_encode(val: &str, kwargs: Kwargs, _: &State) -> TeraResult<String> {
26    let url_safe = kwargs.get::<bool>("url_safe")?.unwrap_or(false);
27    let padded = kwargs.get::<bool>("padded")?.unwrap_or(true);
28    let encoded = match (url_safe, padded) {
29        (false, true) => general_purpose::STANDARD.encode(val),
30        (false, false) => general_purpose::STANDARD_NO_PAD.encode(val),
31        (true, true) => general_purpose::URL_SAFE.encode(val),
32        (true, false) => general_purpose::URL_SAFE_NO_PAD.encode(val),
33    };
34    Ok(encoded)
35}
36
37/// Decodes a base64 string.
38///
39/// ```text
40/// {{ value | b64_decode }}
41/// {{ value | b64_decode(url_safe=true) }}
42/// ```
43pub fn b64_decode(val: &str, kwargs: Kwargs, _: &State) -> TeraResult<String> {
44    let url_safe = kwargs.get::<bool>("url_safe")?.unwrap_or(false);
45    let decoded = if url_safe {
46        URL_SAFE_DECODE.decode(val)
47    } else {
48        STANDARD_DECODE.decode(val)
49    };
50    let bytes = decoded.map_err(|e| tera::Error::message(format!("Invalid base64: {e}")))?;
51    String::from_utf8(bytes).map_err(|e| tera::Error::message(format!("Invalid UTF-8: {e}")))
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use std::sync::Arc;
58    use tera::value::Map;
59    use tera::{Context, Kwargs, State};
60
61    #[test]
62    fn test_b64_encode() {
63        let ctx = Context::new();
64        let state = State::new(&ctx);
65        assert_eq!(
66            b64_encode("hello world", Kwargs::default(), &state).unwrap(),
67            "aGVsbG8gd29ybGQ="
68        );
69    }
70
71    #[test]
72    fn test_b64_encode_no_padding() {
73        let ctx = Context::new();
74        let state = State::new(&ctx);
75        let mut kwargs_map = Map::new();
76        kwargs_map.insert("padded".into(), false.into());
77        let kwargs = Kwargs::new(Arc::new(kwargs_map));
78        assert_eq!(
79            b64_encode("hello world", kwargs, &state).unwrap(),
80            "aGVsbG8gd29ybGQ"
81        );
82    }
83
84    #[test]
85    fn test_b64_encode_url_safe() {
86        let ctx = Context::new();
87        let state = State::new(&ctx);
88        let mut kwargs_map = Map::new();
89        kwargs_map.insert("url_safe".into(), true.into());
90        let kwargs = Kwargs::new(Arc::new(kwargs_map));
91        // String with characters that differ between standard and URL-safe
92        assert_eq!(b64_encode("<<??>>", kwargs, &state).unwrap(), "PDw_Pz4-");
93    }
94
95    #[test]
96    fn test_b64_decode() {
97        let ctx = Context::new();
98        let state = State::new(&ctx);
99        assert_eq!(
100            b64_decode("aGVsbG8gd29ybGQ=", Kwargs::default(), &state).unwrap(),
101            "hello world"
102        );
103        // works as well without padding
104        assert_eq!(
105            b64_decode("aGVsbG8gd29ybGQ", Kwargs::default(), &state).unwrap(),
106            "hello world"
107        );
108    }
109
110    #[test]
111    fn test_b64_decode_url_safe() {
112        let ctx = Context::new();
113        let state = State::new(&ctx);
114        let mut kwargs_map = Map::new();
115        kwargs_map.insert("url_safe".into(), true.into());
116        let kwargs = Kwargs::new(Arc::new(kwargs_map));
117        assert_eq!(b64_decode("PDw_Pz4-", kwargs, &state).unwrap(), "<<??>>");
118    }
119
120    #[test]
121    fn test_b64_roundtrip() {
122        let ctx = Context::new();
123        let state = State::new(&ctx);
124        let original = "Hello, δΈ–η•Œ! 🌍";
125        let encoded = b64_encode(original, Kwargs::default(), &state).unwrap();
126        let decoded = b64_decode(&encoded, Kwargs::default(), &state).unwrap();
127        assert_eq!(decoded, original);
128    }
129
130    #[test]
131    fn test_b64_decode_invalid() {
132        let ctx = Context::new();
133        let state = State::new(&ctx);
134        let result = b64_decode("not valid base64!!!", Kwargs::default(), &state);
135        assert!(result.is_err());
136    }
137
138    #[test]
139    fn test_register() {
140        let mut tera = tera::Tera::default();
141        tera.register_filter("b64_encode", b64_encode);
142        tera.register_filter("b64_decode", b64_decode);
143    }
144}