Skip to main content

txtx_core/std/functions/
base64.rs

1use base64::{engine::general_purpose, Engine};
2use txtx_addon_kit::types::AuthorizationContext;
3use txtx_addon_kit::{
4    define_function, indoc,
5    types::{
6        diagnostics::Diagnostic,
7        functions::{FunctionImplementation, FunctionSpecification},
8        types::{Type, Value},
9    },
10};
11
12lazy_static! {
13    pub static ref FUNCTIONS: Vec<FunctionSpecification> = vec![
14        define_function! {
15            Base64Decode => {
16                name: "decode_base64",
17                documentation: "`decode_base64` decodes a base64 encoded string and returns the result as a buffer.",
18                example: indoc!{r#"
19                output "decoded" {
20                    value = decode_base64("SGVsbG8gd29ybGQh")
21                }
22                > decoded: 0x48656c6c6f20776f726c6421
23              "#},
24                inputs: [
25                    base64_string: {
26                        documentation: "The base64 string to decode.",
27                        typing: vec![Type::string()]
28                    }
29                ],
30                output: {
31                    documentation: "The decoded base64 string, as a buffer.",
32                    typing: Type::buffer()
33                },
34            }
35        },
36        define_function! {
37            Base64Encode => {
38                name: "encode_base64",
39                documentation: "`encode_base64` encodes a buffer or string as a base64 string.",
40                example: indoc!{r#"
41                output "encoded" {
42                    value = encode_base64("0x48656c6c6f20776f726c6421")
43                }
44                > encoded: SGVsbG8gd29ybGQh
45              "#},
46                inputs: [
47                    value: {
48                        documentation: "The buffer or string to encode. Strings starting with '0x' are decoded as hex; otherwise, raw UTF-8 bytes are used.",
49                        typing: vec![Type::buffer(), Type::string(), Type::addon("any")]
50                    }
51                ],
52                output: {
53                    documentation: "The input, encoded as a base64 string.",
54                    typing: Type::string()
55                },
56            }
57        },
58    ];
59}
60
61pub struct Base64Decode;
62impl FunctionImplementation for Base64Decode {
63    fn check_instantiability(
64        _fn_spec: &FunctionSpecification,
65        _auth_ctx: &AuthorizationContext,
66        _args: &Vec<Type>,
67    ) -> Result<Type, Diagnostic> {
68        unimplemented!()
69    }
70
71    fn run(
72        _fn_spec: &FunctionSpecification,
73        _auth_ctx: &AuthorizationContext,
74        args: &Vec<Value>,
75    ) -> Result<Value, Diagnostic> {
76        let encoded = args.get(0).unwrap().expect_string();
77        let decoded = general_purpose::STANDARD.decode(encoded).map_err(|e| {
78            Diagnostic::error_from_string(format!(
79                "failed to decode base64 string {}: {}",
80                encoded, e
81            ))
82        })?;
83        Ok(Value::buffer(decoded))
84    }
85}
86
87/// Helper to get bytes from a Value for encoding functions.
88/// - Buffer: use bytes directly
89/// - String with "0x" prefix: decode as hex
90/// - String without "0x" prefix: use raw UTF-8 bytes
91/// - Addon: use addon bytes
92fn get_bytes_for_encoding(value: &Value) -> Result<Vec<u8>, Diagnostic> {
93    match value {
94        Value::Buffer(b) => Ok(b.clone()),
95        Value::String(s) => {
96            if s.starts_with("0x") {
97                txtx_addon_kit::hex::decode(&s[2..]).map_err(|e| {
98                    Diagnostic::error_from_string(format!("failed to decode hex string: {}", e))
99                })
100            } else {
101                Ok(s.as_bytes().to_vec())
102            }
103        }
104        Value::Addon(addon) => Ok(addon.bytes.clone()),
105        _ => Err(Diagnostic::error_from_string(
106            "expected a buffer, string, or addon value".to_string(),
107        )),
108    }
109}
110
111pub struct Base64Encode;
112impl FunctionImplementation for Base64Encode {
113    fn check_instantiability(
114        _fn_spec: &FunctionSpecification,
115        _auth_ctx: &AuthorizationContext,
116        _args: &Vec<Type>,
117    ) -> Result<Type, Diagnostic> {
118        unimplemented!()
119    }
120
121    fn run(
122        _fn_spec: &FunctionSpecification,
123        _auth_ctx: &AuthorizationContext,
124        args: &Vec<Value>,
125    ) -> Result<Value, Diagnostic> {
126        let bytes = get_bytes_for_encoding(args.get(0).unwrap())?;
127        let encoded = general_purpose::STANDARD.encode(bytes);
128        Ok(Value::string(encoded))
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use test_case::test_case;
135    use txtx_addon_kit::helpers::fs::FileLocation;
136
137    use super::*;
138
139    fn get_spec_by_name(name: &str) -> FunctionSpecification {
140        FUNCTIONS.iter().find(|f| f.name == name).cloned().unwrap()
141    }
142
143    fn dummy_auth_ctx() -> AuthorizationContext {
144        AuthorizationContext { workspace_location: FileLocation::working_dir() }
145    }
146
147    #[test_case(
148        Value::buffer(b"Hello world!".to_vec()),
149        Value::string("SGVsbG8gd29ybGQh".to_string());
150        "buffer hello world"
151    )]
152    #[test_case(
153        Value::string("Hello world!".to_string()),
154        Value::string("SGVsbG8gd29ybGQh".to_string());
155        "plain string hello world"
156    )]
157    #[test_case(
158        Value::string("0x48656c6c6f20776f726c6421".to_string()),
159        Value::string("SGVsbG8gd29ybGQh".to_string());
160        "hex string hello world"
161    )]
162    #[test_case(
163        Value::string("__event_authority".to_string()),
164        Value::string("X19ldmVudF9hdXRob3JpdHk=".to_string());
165        "plain string with underscores"
166    )]
167    #[test_case(
168        Value::buffer(vec![]),
169        Value::string("".to_string());
170        "empty buffer"
171    )]
172    #[test_case(
173        Value::buffer(vec![0, 1, 127, 128, 254, 255]),
174        Value::string("AAF/gP7/".to_string());
175        "binary data with edge bytes"
176    )]
177    fn test_base64_encode_decode_roundtrip(input: Value, expected_encoded: Value) {
178        let encode_spec = get_spec_by_name("encode_base64");
179        let decode_spec = get_spec_by_name("decode_base64");
180        let auth_ctx = dummy_auth_ctx();
181
182        // Encode the input and verify it matches expected
183        let encoded = (encode_spec.runner)(&encode_spec, &auth_ctx, &vec![input.clone()]).unwrap();
184        assert_eq!(encoded, expected_encoded, "encoded value mismatch");
185
186        // Decode the result and verify we get back the original bytes
187        let decoded = (decode_spec.runner)(&decode_spec, &auth_ctx, &vec![encoded]).unwrap();
188
189        // Get expected bytes based on input type
190        let expected_bytes = match &input {
191            Value::Buffer(b) => b.clone(),
192            Value::String(s) if s.starts_with("0x") => {
193                txtx_addon_kit::hex::decode(&s[2..]).unwrap()
194            }
195            Value::String(s) => s.as_bytes().to_vec(),
196            _ => unreachable!(),
197        };
198        assert_eq!(decoded, Value::buffer(expected_bytes), "decoded value mismatch");
199    }
200
201    #[test]
202    fn test_decode_base64_invalid_input() {
203        let fn_spec = get_spec_by_name("decode_base64");
204        let args = vec![Value::string("!!!invalid!!!".to_string())];
205        let result = (fn_spec.runner)(&fn_spec, &dummy_auth_ctx(), &args);
206        assert!(result.is_err());
207    }
208}