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