Skip to main content

txtx_core/std/functions/
hex.rs

1use 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
60/// Helper to get bytes from a Value for encoding functions.
61/// - Buffer: use bytes directly
62/// - String with "0x" prefix: decode as hex
63/// - String without "0x" prefix: use raw UTF-8 bytes
64/// - Addon: use addon bytes
65fn 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        // Encode the input and verify it matches expected
200        let encoded = (encode_spec.runner)(&encode_spec, &auth_ctx, &vec![input.clone()]).unwrap();
201        assert_eq!(encoded, expected_encoded, "encoded value mismatch");
202
203        // Decode the result and verify we get back the original bytes
204        let decoded = (decode_spec.runner)(&decode_spec, &auth_ctx, &vec![encoded]).unwrap();
205
206        // Get expected bytes based on input type
207        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}