vrl/stdlib/
decode_base64.rs

1use crate::compiler::function::EnumVariant;
2use crate::compiler::prelude::*;
3use crate::stdlib::util::Base64Charset;
4use std::sync::LazyLock;
5
6static DEFAULT_CHARSET: LazyLock<Value> = LazyLock::new(|| Value::Bytes(Bytes::from("standard")));
7
8static CHARSET_ENUM: &[EnumVariant] = &[
9    EnumVariant {
10        value: "standard",
11        description: "[Standard](https://tools.ietf.org/html/rfc4648#section-4) Base64 format.",
12    },
13    EnumVariant {
14        value: "url_safe",
15        description: "Modified Base64 for [URL variants](https://en.wikipedia.org/wiki/Base64#URL_applications).",
16    },
17];
18
19static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
20    vec![
21        Parameter::required(
22            "value",
23            kind::BYTES,
24            "The [Base64](https://en.wikipedia.org/wiki/Base64) data to decode.",
25        ),
26        Parameter::optional(
27            "charset",
28            kind::BYTES,
29            "The character set to use when decoding the data.",
30        )
31        .default(&DEFAULT_CHARSET)
32        .enum_variants(CHARSET_ENUM),
33    ]
34});
35
36fn decode_base64(charset: Value, value: Value) -> Resolved {
37    let value = value.try_bytes()?;
38    let charset = Base64Charset::from_slice(&charset.try_bytes()?)?;
39
40    let decoder = match charset {
41        Base64Charset::Standard => base64_simd::STANDARD_NO_PAD,
42        Base64Charset::UrlSafe => base64_simd::URL_SAFE_NO_PAD,
43    };
44
45    // Find the position of padding char '='
46    let pos = value
47        .iter()
48        .rev()
49        .position(|c| *c != b'=')
50        .map_or(value.len(), |p| value.len() - p);
51
52    let decoded_vec = decoder
53        .decode_to_vec(&value[0..pos])
54        .map_err(|_| "unable to decode value from base64")?;
55
56    Ok(Value::Bytes(Bytes::from(decoded_vec)))
57}
58
59#[derive(Clone, Copy, Debug)]
60pub struct DecodeBase64;
61
62impl Function for DecodeBase64 {
63    fn identifier(&self) -> &'static str {
64        "decode_base64"
65    }
66
67    fn usage(&self) -> &'static str {
68        "Decodes the `value` (a [Base64](https://en.wikipedia.org/wiki/Base64) string) into its original string."
69    }
70
71    fn category(&self) -> &'static str {
72        Category::Codec.as_ref()
73    }
74
75    fn internal_failure_reasons(&self) -> &'static [&'static str] {
76        &["`value` isn't a valid encoded Base64 string."]
77    }
78
79    fn return_kind(&self) -> u16 {
80        kind::BYTES
81    }
82
83    fn parameters(&self) -> &'static [Parameter] {
84        PARAMETERS.as_slice()
85    }
86
87    fn compile(
88        &self,
89        _state: &state::TypeState,
90        _ctx: &mut FunctionCompileContext,
91        arguments: ArgumentList,
92    ) -> Compiled {
93        let value = arguments.required("value");
94        let charset = arguments.optional("charset");
95
96        Ok(DecodeBase64Fn { value, charset }.as_expr())
97    }
98
99    fn examples(&self) -> &'static [Example] {
100        &[
101            example! {
102                title: "Decode Base64 data (default)",
103                source: r#"decode_base64!("eW91IGhhdmUgc3VjY2Vzc2Z1bGx5IGRlY29kZWQgbWU=")"#,
104                result: Ok("you have successfully decoded me"),
105            },
106            example! {
107                title: "Decode Base64 data (URL safe)",
108                source: r#"decode_base64!("eW91IGNhbid0IG1ha2UgeW91ciBoZWFydCBmZWVsIHNvbWV0aGluZyBpdCB3b24ndA==", charset: "url_safe")"#,
109                result: Ok("you can't make your heart feel something it won't"),
110            },
111        ]
112    }
113}
114
115#[derive(Clone, Debug)]
116struct DecodeBase64Fn {
117    value: Box<dyn Expression>,
118    charset: Option<Box<dyn Expression>>,
119}
120
121impl FunctionExpression for DecodeBase64Fn {
122    fn resolve(&self, ctx: &mut Context) -> Resolved {
123        let value = self.value.resolve(ctx)?;
124        let charset = self
125            .charset
126            .map_resolve_with_default(ctx, || DEFAULT_CHARSET.clone())?;
127
128        decode_base64(charset, value)
129    }
130
131    fn type_def(&self, _: &state::TypeState) -> TypeDef {
132        // Always fallible due to the possibility of decoding errors that VRL can't detect in
133        // advance: https://docs.rs/base64/0.13.0/base64/enum.DecodeError.html
134        TypeDef::bytes().fallible()
135    }
136}
137
138#[cfg(test)]
139mod test {
140    use super::*;
141    use crate::value;
142
143    test_function![
144        decode_base64 => DecodeBase64;
145
146        with_defaults {
147            args: func_args![value: value!("c29tZSs9c3RyaW5nL3ZhbHVl")],
148            want: Ok(value!("some+=string/value")),
149            tdef: TypeDef::bytes().fallible(),
150        }
151
152        with_standard_charset {
153            args: func_args![value: value!("c29tZSs9c3RyaW5nL3ZhbHVl"), charset: value!["standard"]],
154            want: Ok(value!("some+=string/value")),
155            tdef: TypeDef::bytes().fallible(),
156        }
157
158        with_urlsafe_charset {
159            args: func_args![value: value!("c29tZSs9c3RyaW5nL3ZhbHVl"), charset: value!("url_safe")],
160            want: Ok(value!("some+=string/value")),
161            tdef: TypeDef::bytes().fallible(),
162        }
163
164        with_invalid_charset {
165            args: func_args![value: value!("c29tZSs9c3RyaW5nL3ZhbHVl"), charset: value!("invalid")],
166            want: Err("unknown charset"),
167            tdef: TypeDef::bytes().fallible(),
168        }
169
170        with_defaults_invalid_value {
171            args: func_args![value: value!("helloworld")],
172            want: Err("unable to decode value from base64"),
173            tdef: TypeDef::bytes().fallible(),
174        }
175
176        empty_string_standard_charset {
177            args: func_args![value: value!(""), charset: value!("standard")],
178            want: Ok(value!("")),
179            tdef: TypeDef::bytes().fallible(),
180        }
181
182        empty_string_urlsafe_charset {
183            args: func_args![value: value!(""), charset: value!("url_safe")],
184            want: Ok(value!("")),
185            tdef: TypeDef::bytes().fallible(),
186        }
187
188        // decode_base64 function should be able to decode base64 string with or without padding
189        padding_not_included {
190            args: func_args![value: value!("c29tZSs9c3RyaW5nL3ZhbHVlXw")],
191            want: Ok(value!("some+=string/value_")),
192            tdef: TypeDef::bytes().fallible(),
193        }
194
195        padding_included {
196            args: func_args![value: value!("c29tZSs9c3RyaW5nL3ZhbHVlXw==")],
197            want: Ok(value!("some+=string/value_")),
198            tdef: TypeDef::bytes().fallible(),
199        }
200
201        // https://github.com/vectordotdev/vrl/issues/959
202        no_padding {
203            args: func_args![value: value!("eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy91bnN0cnVjdF9ldmVudC9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9saW5rX2NsaWNrL2pzb25zY2hlbWEvMS0wLTEiLCJkYXRhIjp7InRhcmdldFVybCI6Imh0dHBzOi8vaWRwLWF1dGguZ2FyLmVkdWNhdGlvbi5mci9kb21haW5lR2FyP2lkRU5UPVNqQT0maWRTcmM9WVhKck9pODBPRFUyTmk5d2RERTRNREF3TVE9PSIsImVsZW1lbnRJZCI6IiIsImVsZW1lbnRDbGFzc2VzIjpbImxpbmstYnV0dG9uIiwidHJhY2tlZCJdLCJlbGVtZW50VGFyZ2V0IjoiX2JsYW5rIn19fQ")],
204            want: Ok(value!(r#"{"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{"schema":"iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1","data":{"targetUrl":"https://idp-auth.gar.education.fr/domaineGar?idENT=SjA=&idSrc=YXJrOi80ODU2Ni9wdDE4MDAwMQ==","elementId":"","elementClasses":["link-button","tracked"],"elementTarget":"_blank"}}}"#)),
205            tdef: TypeDef::bytes().fallible(),
206        }
207    ];
208}