txtx_core/std/functions/
base64.rs1use 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
87fn 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 let encoded = (encode_spec.runner)(&encode_spec, &auth_ctx, &vec![input.clone()]).unwrap();
184 assert_eq!(encoded, expected_encoded, "encoded value mismatch");
185
186 let decoded = (decode_spec.runner)(&decode_spec, &auth_ctx, &vec![encoded]).unwrap();
188
189 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}