Skip to main content

fakecloud_stepfunctions/
intrinsics.rs

1//! Amazon States Language intrinsic functions.
2//!
3//! These appear in Parameters / ResultSelector / Arguments / Output
4//! values when the JSON key uses the `.$` suffix and the value is a
5//! string starting with `States.`. This module parses the call,
6//! resolves arguments (JSONPath references vs JSON literals), and
7//! returns the computed value.
8//!
9//! Reference: https://docs.aws.amazon.com/step-functions/latest/dg/intrinsic-functions.html
10
11use base64::Engine;
12use serde_json::{json, Value};
13
14#[derive(Debug, Clone)]
15pub struct IntrinsicError(pub String);
16
17impl std::fmt::Display for IntrinsicError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(f, "{}", self.0)
20    }
21}
22
23/// Returns true if `value` is a string that should be evaluated as an
24/// intrinsic (`States.Foo(...)`) rather than a JSONPath reference.
25pub fn is_intrinsic_call(value: &str) -> bool {
26    value.starts_with("States.") && value.contains('(')
27}
28
29/// Maximum nesting depth of intrinsic calls within one field. AWS: "You can
30/// nest up to 10 intrinsic functions within a field."
31const MAX_NESTING: usize = 10;
32
33/// Failure evaluating an intrinsic expression.
34#[derive(Debug, Clone)]
35pub enum CallError {
36    /// The function itself failed (bad arguments, unknown function, ...):
37    /// surfaced as `States.Runtime`, naming the call, as recorded.
38    Intrinsic(IntrinsicError),
39    /// A JSONPath argument matched nothing: surfaced as `States.Runtime`, as
40    /// for every other path that matches nothing.
41    Path(String),
42}
43
44impl CallError {
45    /// The `(error, cause)` pair a state fails with when evaluating `call`. As
46    /// recorded, a failing function is a `States.Runtime` failure, which no
47    /// `Retry` or `Catch` wildcard takes: `There was an error while evaluating the
48    /// intrinsic function: States.MathAdd($.s, 1). Invalid arguments in
49    /// States.MathAdd`.
50    pub fn into_states_error(self, call: &str) -> (String, String) {
51        match self {
52            CallError::Intrinsic(e) => (
53                "States.Runtime".to_string(),
54                format!(
55                    "There was an error while evaluating the intrinsic function: {call}. {}",
56                    e.0
57                ),
58            ),
59            CallError::Path(cause) => ("States.Runtime".to_string(), cause),
60        }
61    }
62}
63
64impl From<IntrinsicError> for CallError {
65    fn from(e: IntrinsicError) -> Self {
66        CallError::Intrinsic(e)
67    }
68}
69
70/// Evaluate an ASL intrinsic call against `input`. Returns the
71/// computed value or an error string suitable for surfacing as
72/// a failing state.
73pub fn evaluate(call: &str, input: &Value) -> Result<Value, IntrinsicError> {
74    evaluate_with_context(call, input, None).map_err(|e| match e {
75        CallError::Intrinsic(e) => e,
76        CallError::Path(cause) => IntrinsicError(cause),
77    })
78}
79
80/// Evaluate an intrinsic call whose `$$` arguments read `context` (the
81/// Step Functions context object) when one is supplied.
82pub fn evaluate_with_context(
83    call: &str,
84    input: &Value,
85    context: Option<&Value>,
86) -> Result<Value, CallError> {
87    let mut parser = CallParser {
88        chars: call.trim().chars().collect(),
89        pos: 0,
90    };
91    let expr = parser.call(1)?;
92    parser.skip_ws();
93    if parser.pos != parser.chars.len() {
94        return Err(IntrinsicError(format!("unexpected trailing input in '{call}'")).into());
95    }
96    eval_expr(&expr, input, context)
97}
98
99/// A parsed intrinsic argument.
100#[derive(Debug, Clone)]
101enum Expr {
102    Call {
103        name: String,
104        args: Vec<Expr>,
105    },
106    /// A single-quoted string literal: its unescaped value, and the raw text
107    /// between the quotes (States.Format needs the escapes to tell `\{\}`
108    /// apart from a `{}` placeholder).
109    Str {
110        value: String,
111        raw: String,
112    },
113    Path(String),
114    Literal(Value),
115}
116
117fn eval_expr(expr: &Expr, input: &Value, context: Option<&Value>) -> Result<Value, CallError> {
118    match expr {
119        Expr::Literal(v) => Ok(v.clone()),
120        Expr::Str { value, .. } => Ok(Value::String(value.clone())),
121        Expr::Path(path) => crate::io_processing::resolve_with_context(input, context, path)
122            .map_err(|e| CallError::Path(crate::io_processing::path_error_cause(path, &e))),
123        Expr::Call { name, args } => {
124            let values = args
125                .iter()
126                .map(|a| eval_expr(a, input, context))
127                .collect::<Result<Vec<_>, _>>()?;
128            if name == "States.Format" {
129                let raw = match args.first() {
130                    Some(Expr::Str { raw, .. }) => Some(raw.as_str()),
131                    _ => None,
132                };
133                return Ok(fn_format(raw, &values)?);
134            }
135            Ok(call_function(name, &values)?)
136        }
137    }
138}
139
140fn call_function(name: &str, args: &[Value]) -> Result<Value, IntrinsicError> {
141    match name {
142        "States.JsonToString" => fn_json_to_string(args),
143        "States.StringToJson" => fn_string_to_json(args),
144        "States.Array" => Ok(Value::Array(args.to_vec())),
145        "States.ArrayPartition" => fn_array_partition(args),
146        "States.ArrayContains" => fn_array_contains(args),
147        "States.ArrayRange" => fn_array_range(args),
148        "States.ArrayGetItem" => fn_array_get_item(args),
149        "States.ArrayLength" => fn_array_length(args),
150        "States.ArrayUnique" => fn_array_unique(args),
151        "States.Base64Encode" => fn_base64_encode(args),
152        "States.Base64Decode" => fn_base64_decode(args),
153        "States.Hash" => fn_hash(args),
154        "States.JsonMerge" => fn_json_merge(args),
155        "States.MathRandom" => fn_math_random(args),
156        "States.MathAdd" => fn_math_add(args),
157        "States.UUID" => fn_uuid(args),
158        "States.StringSplit" => fn_string_split(args),
159        other => Err(IntrinsicError(format!("unknown intrinsic '{other}'"))),
160    }
161}
162
163/// Recursive-descent parser for `States.Fn(arg, ...)` expressions. Arguments
164/// are nested calls, single-quoted strings (escapes `\'`, `\{`, `\}`, `\\`),
165/// JSONPath references (`$...` / `$$...`, which may themselves contain
166/// brackets, parentheses, commas and quotes inside filters) or JSON literals
167/// (numbers, `true`, `false`, `null`).
168struct CallParser {
169    chars: Vec<char>,
170    pos: usize,
171}
172
173impl CallParser {
174    fn peek(&self) -> Option<char> {
175        self.chars.get(self.pos).copied()
176    }
177
178    fn skip_ws(&mut self) {
179        while self.peek().is_some_and(char::is_whitespace) {
180            self.pos += 1;
181        }
182    }
183
184    fn rest_starts_with(&self, s: &str) -> bool {
185        let n = s.chars().count();
186        self.chars
187            .get(self.pos..self.pos + n)
188            .is_some_and(|w| w.iter().copied().eq(s.chars()))
189    }
190
191    fn text(&self) -> String {
192        self.chars.iter().collect()
193    }
194
195    fn err<T>(&self, msg: &str) -> Result<T, IntrinsicError> {
196        Err(IntrinsicError(format!(
197            "{msg} at position {} in '{}'",
198            self.pos,
199            self.text()
200        )))
201    }
202
203    fn call(&mut self, depth: usize) -> Result<Expr, IntrinsicError> {
204        if depth > MAX_NESTING {
205            return Err(IntrinsicError(format!(
206                "intrinsic functions can be nested at most {MAX_NESTING} levels deep in '{}'",
207                self.text()
208            )));
209        }
210        self.skip_ws();
211        if !self.rest_starts_with("States.") {
212            return self.err("expected an intrinsic function");
213        }
214        let start = self.pos;
215        while self
216            .peek()
217            .is_some_and(|c| c.is_ascii_alphanumeric() || c == '.')
218        {
219            self.pos += 1;
220        }
221        let name: String = self.chars[start..self.pos].iter().collect();
222        self.skip_ws();
223        if self.peek() != Some('(') {
224            return Err(IntrinsicError(format!("missing '(' in '{}'", self.text())));
225        }
226        self.pos += 1;
227        let mut args = Vec::new();
228        self.skip_ws();
229        if self.peek() == Some(')') {
230            self.pos += 1;
231            return Ok(Expr::Call { name, args });
232        }
233        loop {
234            args.push(self.arg(depth)?);
235            self.skip_ws();
236            match self.peek() {
237                Some(',') => self.pos += 1,
238                Some(')') => {
239                    self.pos += 1;
240                    return Ok(Expr::Call { name, args });
241                }
242                None => return Err(IntrinsicError(format!("missing ')' in '{}'", self.text()))),
243                Some(_) => return self.err("expected ',' or ')'"),
244            }
245        }
246    }
247
248    fn arg(&mut self, depth: usize) -> Result<Expr, IntrinsicError> {
249        self.skip_ws();
250        match self.peek() {
251            Some('S') if self.rest_starts_with("States.") => self.call(depth + 1),
252            Some('\'') => self.string(),
253            Some('$') => self.path(),
254            Some(_) => self.literal(),
255            None => self.err("expected an argument"),
256        }
257    }
258
259    fn string(&mut self) -> Result<Expr, IntrinsicError> {
260        self.pos += 1; // opening quote
261        let mut value = String::new();
262        let mut raw = String::new();
263        loop {
264            match self.peek() {
265                None => return self.err("unterminated string literal"),
266                Some('\\') => {
267                    let Some(next) = self.chars.get(self.pos + 1).copied() else {
268                        return self.err("unterminated escape sequence");
269                    };
270                    raw.push('\\');
271                    raw.push(next);
272                    value.push_str(&unescape_char(next));
273                    self.pos += 2;
274                }
275                Some('\'') => {
276                    self.pos += 1;
277                    return Ok(Expr::Str { value, raw });
278                }
279                Some(c) => {
280                    value.push(c);
281                    raw.push(c);
282                    self.pos += 1;
283                }
284            }
285        }
286    }
287
288    /// A JSONPath runs to the next top-level `,` or `)`; brackets,
289    /// parentheses and quotes inside it (filters, quoted names) are skipped.
290    fn path(&mut self) -> Result<Expr, IntrinsicError> {
291        let start = self.pos;
292        let mut depth = 0usize;
293        let mut quote: Option<char> = None;
294        while let Some(c) = self.peek() {
295            if let Some(q) = quote {
296                if c == '\\' {
297                    self.pos += 1;
298                } else if c == q {
299                    quote = None;
300                }
301            } else {
302                match c {
303                    '\'' | '"' if depth > 0 => quote = Some(c),
304                    '[' | '(' => depth += 1,
305                    ']' | ')' if depth > 0 => depth -= 1,
306                    ',' | ')' if depth == 0 => break,
307                    _ => {}
308                }
309            }
310            self.pos += 1;
311        }
312        let path: String = self.chars[start..self.pos.min(self.chars.len())]
313            .iter()
314            .collect();
315        Ok(Expr::Path(path.trim_end().to_string()))
316    }
317
318    fn literal(&mut self) -> Result<Expr, IntrinsicError> {
319        let start = self.pos;
320        while self.peek().is_some_and(|c| c != ',' && c != ')') {
321            self.pos += 1;
322        }
323        let raw: String = self.chars[start..self.pos].iter().collect();
324        let raw = raw.trim();
325        serde_json::from_str::<Value>(raw)
326            .map(Expr::Literal)
327            .map_err(|e| IntrinsicError(format!("invalid argument '{raw}': {e}")))
328    }
329}
330
331/// Value of the character following a backslash in a string literal.
332fn unescape_char(c: char) -> String {
333    match c {
334        '\\' | '\'' | '{' | '}' => c.to_string(),
335        'n' => "\n".to_string(),
336        't' => "\t".to_string(),
337        other => format!("\\{other}"),
338    }
339}
340
341fn arg_as_str(v: &Value) -> Result<String, IntrinsicError> {
342    match v {
343        Value::String(s) => Ok(s.clone()),
344        other => Ok(serde_json::to_string(other).unwrap_or_default()),
345    }
346}
347
348fn arg_as_array(v: &Value) -> Result<&Vec<Value>, IntrinsicError> {
349    v.as_array()
350        .ok_or_else(|| IntrinsicError(format!("expected array, got {v}")))
351}
352
353fn arg_as_i64(v: &Value) -> Result<i64, IntrinsicError> {
354    v.as_i64()
355        .or_else(|| v.as_f64().map(|f| f as i64))
356        .ok_or_else(|| IntrinsicError(format!("expected integer, got {v}")))
357}
358
359fn arg_as_f64(v: &Value) -> Result<f64, IntrinsicError> {
360    v.as_f64()
361        .ok_or_else(|| IntrinsicError(format!("expected number, got {v}")))
362}
363
364fn need_args(args: &[Value], expected: usize, name: &str) -> Result<(), IntrinsicError> {
365    if args.len() != expected {
366        Err(IntrinsicError(format!(
367            "{name} expected {expected} args, got {}",
368            args.len()
369        )))
370    } else {
371        Ok(())
372    }
373}
374
375/// `States.Format(template, args...)`. Each `{}` in the template is
376/// replaced by the next argument. In a string-literal template, `\{` and `\}`
377/// are literal braces (so `'\{\}'` is not a placeholder); `raw` carries the
378/// literal's escaped text for that. A template from a path or nested call is
379/// used as-is.
380fn fn_format(raw: Option<&str>, args: &[Value]) -> Result<Value, IntrinsicError> {
381    if args.is_empty() {
382        return Err(IntrinsicError(
383            "States.Format requires at least one argument".into(),
384        ));
385    }
386    let template = match raw {
387        Some(raw) => raw.to_string(),
388        None => args[0]
389            .as_str()
390            .ok_or_else(|| IntrinsicError("States.Format template must be a string".into()))?
391            .to_string(),
392    };
393    let honour_escapes = raw.is_some();
394    let mut out = String::with_capacity(template.len());
395    let mut chars = template.chars().peekable();
396    let mut idx = 1;
397    while let Some(c) = chars.next() {
398        match c {
399            '\\' if honour_escapes => {
400                if let Some(n) = chars.next() {
401                    out.push_str(&unescape_char(n));
402                }
403            }
404            '{' if matches!(chars.peek(), Some('}')) => {
405                chars.next();
406                let v = args.get(idx).ok_or_else(format_arity_error)?;
407                idx += 1;
408                match v {
409                    Value::String(s) => out.push_str(s),
410                    Value::Null => out.push_str("null"),
411                    other => out.push_str(&serde_json::to_string(other).unwrap_or_default()),
412                }
413            }
414            _ => out.push(c),
415        }
416    }
417    if idx != args.len() {
418        return Err(format_arity_error());
419    }
420    Ok(Value::String(out))
421}
422
423/// As recorded, for a template with more placeholders than arguments.
424fn format_arity_error() -> IntrinsicError {
425    IntrinsicError(
426        "Invalid arguments in States.Format: number of arguments do not match the occurrences \
427         of {}."
428            .into(),
429    )
430}
431
432fn fn_json_to_string(args: &[Value]) -> Result<Value, IntrinsicError> {
433    need_args(args, 1, "States.JsonToString")?;
434    Ok(Value::String(
435        serde_json::to_string(&args[0]).unwrap_or_default(),
436    ))
437}
438
439fn fn_string_to_json(args: &[Value]) -> Result<Value, IntrinsicError> {
440    need_args(args, 1, "States.StringToJson")?;
441    let s = args[0]
442        .as_str()
443        .ok_or_else(|| IntrinsicError("States.StringToJson arg must be a string".into()))?;
444    serde_json::from_str(s).map_err(|e| {
445        IntrinsicError(format!(
446            "Cannot convert String to Json in States.StringToJson, caused by: {e}"
447        ))
448    })
449}
450
451fn fn_array_partition(args: &[Value]) -> Result<Value, IntrinsicError> {
452    need_args(args, 2, "States.ArrayPartition")?;
453    let arr = arg_as_array(&args[0])?;
454    let chunk = arg_as_i64(&args[1])?;
455    if chunk <= 0 {
456        return Err(IntrinsicError(
457            "ArrayPartition chunk size must be > 0".into(),
458        ));
459    }
460    let chunk = chunk as usize;
461    let mut out: Vec<Value> = Vec::new();
462    for slice in arr.chunks(chunk) {
463        out.push(Value::Array(slice.to_vec()));
464    }
465    Ok(Value::Array(out))
466}
467
468fn fn_array_contains(args: &[Value]) -> Result<Value, IntrinsicError> {
469    need_args(args, 2, "States.ArrayContains")?;
470    let arr = arg_as_array(&args[0])?;
471    Ok(Value::Bool(arr.iter().any(|v| v == &args[1])))
472}
473
474/// AWS: "The array returned by States.ArrayRange can contain up to 1000 items."
475const ARRAY_RANGE_MAX_ITEMS: i128 = 1000;
476
477fn fn_array_range(args: &[Value]) -> Result<Value, IntrinsicError> {
478    need_args(args, 3, "States.ArrayRange")?;
479    let start = arg_as_i64(&args[0])?;
480    let end = arg_as_i64(&args[1])?;
481    let step = arg_as_i64(&args[2])?;
482    if step == 0 {
483        // As recorded.
484        return Err(IntrinsicError(
485            "Invalid arguments in States.ArrayRange".into(),
486        ));
487    }
488    // Compute the element count in i128 so extreme operands cannot overflow,
489    // and enforce the AWS cap before allocating anything.
490    let span = i128::from(end) - i128::from(start);
491    let count: i128 = if (step > 0 && span < 0) || (step < 0 && span > 0) {
492        0
493    } else {
494        span / i128::from(step) + 1
495    };
496    if count > ARRAY_RANGE_MAX_ITEMS {
497        return Err(IntrinsicError(format!(
498            "ArrayRange result exceeds the maximum of {ARRAY_RANGE_MAX_ITEMS} items"
499        )));
500    }
501    let out: Vec<Value> = (0..count)
502        .map(|k| json!((i128::from(start) + k * i128::from(step)) as i64))
503        .collect();
504    Ok(Value::Array(out))
505}
506
507fn fn_array_get_item(args: &[Value]) -> Result<Value, IntrinsicError> {
508    need_args(args, 2, "States.ArrayGetItem")?;
509    let arr = arg_as_array(&args[0])?;
510    let idx = arg_as_i64(&args[1])?;
511    if idx < 0 {
512        return Err(IntrinsicError("ArrayGetItem index must be >= 0".into()));
513    }
514    Ok(arr.get(idx as usize).cloned().unwrap_or(Value::Null))
515}
516
517fn fn_array_length(args: &[Value]) -> Result<Value, IntrinsicError> {
518    need_args(args, 1, "States.ArrayLength")?;
519    let arr = arg_as_array(&args[0])?;
520    Ok(json!(arr.len()))
521}
522
523fn fn_array_unique(args: &[Value]) -> Result<Value, IntrinsicError> {
524    need_args(args, 1, "States.ArrayUnique")?;
525    let arr = arg_as_array(&args[0])?;
526    let mut seen: Vec<Value> = Vec::new();
527    for v in arr {
528        if !seen.contains(v) {
529            seen.push(v.clone());
530        }
531    }
532    Ok(Value::Array(seen))
533}
534
535fn fn_base64_encode(args: &[Value]) -> Result<Value, IntrinsicError> {
536    need_args(args, 1, "States.Base64Encode")?;
537    let s = arg_as_str(&args[0])?;
538    Ok(Value::String(
539        base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
540    ))
541}
542
543fn fn_base64_decode(args: &[Value]) -> Result<Value, IntrinsicError> {
544    need_args(args, 1, "States.Base64Decode")?;
545    let s = arg_as_str(&args[0])?;
546    // As recorded, characters outside the alphabet are skipped: `%%%` decodes
547    // to the empty string.
548    let alphabet: Vec<u8> = s
549        .bytes()
550        .filter(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'='))
551        .collect();
552    let bytes = base64::engine::general_purpose::STANDARD
553        .decode(&alphabet)
554        .map_err(|e| IntrinsicError(format!("Base64Decode failed: {e}")))?;
555    let decoded = String::from_utf8(bytes)
556        .map_err(|e| IntrinsicError(format!("Base64Decode utf8 failed: {e}")))?;
557    Ok(Value::String(decoded))
558}
559
560fn fn_hash(args: &[Value]) -> Result<Value, IntrinsicError> {
561    use md5::Digest;
562    need_args(args, 2, "States.Hash")?;
563    let input = arg_as_str(&args[0])?;
564    let algo = arg_as_str(&args[1])?;
565    let digest_hex = match algo.as_str() {
566        "MD5" => {
567            let mut h = md5::Md5::new();
568            h.update(input.as_bytes());
569            hex::encode(h.finalize())
570        }
571        "SHA-1" => {
572            let mut h = sha1::Sha1::new();
573            h.update(input.as_bytes());
574            hex::encode(h.finalize())
575        }
576        "SHA-256" => {
577            let mut h = sha2::Sha256::new();
578            h.update(input.as_bytes());
579            hex::encode(h.finalize())
580        }
581        "SHA-384" => {
582            let mut h = sha2::Sha384::new();
583            h.update(input.as_bytes());
584            hex::encode(h.finalize())
585        }
586        "SHA-512" => {
587            let mut h = sha2::Sha512::new();
588            h.update(input.as_bytes());
589            hex::encode(h.finalize())
590        }
591        other => {
592            return Err(IntrinsicError(format!(
593                "unsupported hash algorithm '{other}'"
594            )))
595        }
596    };
597    Ok(Value::String(digest_hex))
598}
599
600fn fn_json_merge(args: &[Value]) -> Result<Value, IntrinsicError> {
601    need_args(args, 3, "States.JsonMerge")?;
602    let a = args[0]
603        .as_object()
604        .ok_or_else(|| IntrinsicError("JsonMerge arg 1 must be object".into()))?;
605    let b = args[1]
606        .as_object()
607        .ok_or_else(|| IntrinsicError("JsonMerge arg 2 must be object".into()))?;
608    let deep = args[2]
609        .as_bool()
610        .ok_or_else(|| IntrinsicError("JsonMerge arg 3 must be bool".into()))?;
611    let mut merged = a.clone();
612    if deep {
613        deep_merge(&mut merged, b);
614    } else {
615        for (k, v) in b {
616            merged.insert(k.clone(), v.clone());
617        }
618    }
619    Ok(Value::Object(merged))
620}
621
622fn deep_merge(a: &mut serde_json::Map<String, Value>, b: &serde_json::Map<String, Value>) {
623    for (k, v) in b {
624        match (a.get_mut(k), v) {
625            (Some(Value::Object(am)), Value::Object(bm)) => deep_merge(am, bm),
626            _ => {
627                a.insert(k.clone(), v.clone());
628            }
629        }
630    }
631}
632
633fn fn_math_random(args: &[Value]) -> Result<Value, IntrinsicError> {
634    use rand::Rng;
635    if args.len() < 2 || args.len() > 3 {
636        return Err(IntrinsicError(
637            "States.MathRandom expected 2 or 3 args".into(),
638        ));
639    }
640    let start = arg_as_i64(&args[0])?;
641    let end = arg_as_i64(&args[1])?;
642    if end <= start {
643        return Err(IntrinsicError("MathRandom end must be > start".into()));
644    }
645    // 3rd arg is an optional seed; we honour it for deterministic tests.
646    let v: i64 = if let Some(seed_v) = args.get(2) {
647        use rand::SeedableRng;
648        let seed = arg_as_i64(seed_v)? as u64;
649        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
650        rng.gen_range(start..end)
651    } else {
652        rand::thread_rng().gen_range(start..end)
653    };
654    Ok(json!(v))
655}
656
657fn fn_math_add(args: &[Value]) -> Result<Value, IntrinsicError> {
658    need_args(args, 2, "States.MathAdd")?;
659    // Integer operands add with 64-bit integer semantics, but a genuine
660    // overflow errors instead of panicking (debug) or silently wrapping
661    // (release).
662    if let (Some(a), Some(b)) = (args[0].as_i64(), args[1].as_i64()) {
663        return match a.checked_add(b) {
664            Some(sum) => Ok(json!(sum)),
665            None => Err(IntrinsicError(
666                "States.MathAdd result overflows a 64-bit integer".into(),
667            )),
668        };
669    }
670    // Otherwise fall back to floating-point addition. This covers fractional
671    // operands (which the old i64 coercion truncated) and integers too large
672    // for i64. AWS treats numbers as given, so no truncation is applied.
673    // As recorded for a non-number operand.
674    let invalid = |_| IntrinsicError("Invalid arguments in States.MathAdd".into());
675    let a = arg_as_f64(&args[0]).map_err(invalid)?;
676    let b = arg_as_f64(&args[1]).map_err(invalid)?;
677    Ok(json!(a + b))
678}
679
680fn fn_uuid(args: &[Value]) -> Result<Value, IntrinsicError> {
681    need_args(args, 0, "States.UUID")?;
682    Ok(Value::String(uuid::Uuid::new_v4().to_string()))
683}
684
685fn fn_string_split(args: &[Value]) -> Result<Value, IntrinsicError> {
686    need_args(args, 2, "States.StringSplit")?;
687    let s = arg_as_str(&args[0])?;
688    let splitter = arg_as_str(&args[1])?;
689    if splitter.is_empty() {
690        return Err(IntrinsicError(
691            "StringSplit delimiter must be non-empty".into(),
692        ));
693    }
694    // ASL StringSplit treats every char in the delimiter as a possible
695    // separator (eg. delimiter "., " splits on either dot, comma, or
696    // space) and drops empty tokens.
697    let chars: Vec<char> = splitter.chars().collect();
698    let parts: Vec<Value> = s
699        .split(|c: char| chars.contains(&c))
700        .filter(|p| !p.is_empty())
701        .map(|p| Value::String(p.to_string()))
702        .collect();
703    Ok(Value::Array(parts))
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use serde_json::json;
710
711    #[test]
712    fn format_substitutes_placeholders() {
713        let out = evaluate("States.Format('Hello, {}!', 'Alice')", &Value::Null).unwrap();
714        assert_eq!(out, json!("Hello, Alice!"));
715    }
716
717    #[test]
718    fn format_resolves_jsonpath_args() {
719        let input = json!({"name": "Bob", "n": 3});
720        let out = evaluate("States.Format('{}={}', $.name, $.n)", &input).unwrap();
721        assert_eq!(out, json!("Bob=3"));
722    }
723
724    #[test]
725    fn array_intrinsics() {
726        assert_eq!(
727            evaluate("States.Array(1, 2, 3)", &Value::Null).unwrap(),
728            json!([1, 2, 3])
729        );
730        assert_eq!(
731            evaluate("States.ArrayLength($)", &json!([10, 20, 30])).unwrap(),
732            json!(3)
733        );
734        assert_eq!(
735            evaluate("States.ArrayContains($, 2)", &json!([1, 2, 3])).unwrap(),
736            json!(true)
737        );
738        assert_eq!(
739            evaluate("States.ArrayContains($, 9)", &json!([1, 2, 3])).unwrap(),
740            json!(false)
741        );
742        assert_eq!(
743            evaluate("States.ArrayRange(1, 9, 2)", &Value::Null).unwrap(),
744            json!([1, 3, 5, 7, 9])
745        );
746        assert_eq!(
747            evaluate("States.ArrayPartition($, 2)", &json!([1, 2, 3, 4, 5])).unwrap(),
748            json!([[1, 2], [3, 4], [5]])
749        );
750        assert_eq!(
751            evaluate("States.ArrayGetItem($, 1)", &json!(["a", "b", "c"])).unwrap(),
752            json!("b")
753        );
754        assert_eq!(
755            evaluate("States.ArrayUnique($)", &json!([1, 2, 1, 3, 2])).unwrap(),
756            json!([1, 2, 3])
757        );
758    }
759
760    #[test]
761    fn json_intrinsics() {
762        assert_eq!(
763            evaluate("States.JsonToString($)", &json!({"x": 1})).unwrap(),
764            json!(r#"{"x":1}"#)
765        );
766        assert_eq!(
767            evaluate("States.StringToJson($)", &json!(r#"{"x":1}"#)).unwrap(),
768            json!({"x": 1})
769        );
770        assert_eq!(
771            evaluate(
772                "States.JsonMerge($.a, $.b, false)",
773                &json!({"a": {"x": 1, "y": 2}, "b": {"y": 9, "z": 3}})
774            )
775            .unwrap(),
776            json!({"x": 1, "y": 9, "z": 3})
777        );
778    }
779
780    #[test]
781    fn base64_intrinsics() {
782        let enc = evaluate("States.Base64Encode('hello')", &Value::Null).unwrap();
783        assert_eq!(enc, json!("aGVsbG8="));
784        let dec = evaluate("States.Base64Decode('aGVsbG8=')", &Value::Null).unwrap();
785        assert_eq!(dec, json!("hello"));
786    }
787
788    #[test]
789    fn hash_intrinsic() {
790        let out = evaluate("States.Hash('hello', 'SHA-256')", &Value::Null).unwrap();
791        assert_eq!(
792            out,
793            json!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
794        );
795    }
796
797    #[test]
798    fn math_intrinsics() {
799        assert_eq!(
800            evaluate("States.MathAdd(2, 3)", &Value::Null).unwrap(),
801            json!(5)
802        );
803        let r = evaluate("States.MathRandom(0, 10)", &Value::Null).unwrap();
804        let n = r.as_i64().unwrap();
805        assert!((0..10).contains(&n));
806    }
807
808    // L6: MathAdd must not panic (debug) or wrap (release) on i64 overflow, and
809    // must not truncate fractional operands.
810    #[test]
811    fn math_add_overflow_and_floats() {
812        // i64::MAX + 1 overflows → error rather than panic/wrap.
813        let expr = format!("States.MathAdd({}, 1)", i64::MAX);
814        assert!(evaluate(&expr, &Value::Null).is_err());
815
816        // Fractional operands are preserved, not truncated to int.
817        assert_eq!(
818            fn_math_add(&[json!(1.5), json!(2.25)]).unwrap(),
819            json!(3.75)
820        );
821        // Mixed int + float stays a float.
822        assert_eq!(fn_math_add(&[json!(2), json!(0.5)]).unwrap(), json!(2.5));
823        // Negative integers still work.
824        assert_eq!(fn_math_add(&[json!(-4), json!(1)]).unwrap(), json!(-3));
825    }
826
827    #[test]
828    fn uuid_intrinsic_is_v4() {
829        let out = evaluate("States.UUID()", &Value::Null).unwrap();
830        let s = out.as_str().unwrap();
831        // 8-4-4-4-12 = 36 chars total
832        assert_eq!(s.len(), 36);
833        assert_eq!(s.chars().nth(14).unwrap(), '4');
834    }
835
836    #[test]
837    fn string_split_intrinsic() {
838        assert_eq!(
839            evaluate("States.StringSplit('a,b,c', ',')", &Value::Null).unwrap(),
840            json!(["a", "b", "c"])
841        );
842        // Multi-char delimiter splits on any contained char and drops
843        // empties.
844        assert_eq!(
845            evaluate("States.StringSplit('a,b c', ', ')", &Value::Null).unwrap(),
846            json!(["a", "b", "c"])
847        );
848    }
849
850    #[test]
851    fn detects_intrinsic_call() {
852        assert!(is_intrinsic_call("States.UUID()"));
853        assert!(is_intrinsic_call("States.Format('{}', $.x)"));
854        assert!(!is_intrinsic_call("$.foo.bar"));
855        assert!(!is_intrinsic_call("States.IntrinsicFailure"));
856    }
857
858    #[test]
859    fn unknown_intrinsic_errors() {
860        let err = evaluate("States.NoSuchFunction()", &Value::Null).unwrap_err();
861        assert!(format!("{err}").contains("unknown"));
862    }
863
864    #[test]
865    fn array_range_extreme_operands_do_not_panic() {
866        let input = json!({"min": i64::MIN, "max": i64::MAX, "near": i64::MAX - 1});
867        // Unbounded span is rejected by the 1000-item cap, not by OOM.
868        let err = evaluate("States.ArrayRange($.min, $.max, 1)", &input).unwrap_err();
869        assert!(format!("{err}").contains("1000"), "{err}");
870        // A step that would overflow `i += step` yields the in-range items only.
871        assert_eq!(
872            evaluate("States.ArrayRange($.near, $.max, $.max)", &input).unwrap(),
873            json!([i64::MAX - 1])
874        );
875        assert_eq!(
876            evaluate("States.ArrayRange($.min, $.max, $.max)", &input).unwrap(),
877            json!([i64::MIN, -1, i64::MAX - 1])
878        );
879        // Negative step down to i64::MIN.
880        assert_eq!(
881            evaluate("States.ArrayRange($.max, $.min, $.min)", &input).unwrap(),
882            json!([i64::MAX, -1])
883        );
884        // Exactly 1000 items is allowed; 1001 is not.
885        let ok = evaluate("States.ArrayRange(1, 1000, 1)", &Value::Null).unwrap();
886        assert_eq!(ok.as_array().unwrap().len(), 1000);
887        assert!(evaluate("States.ArrayRange(1, 1001, 1)", &Value::Null).is_err());
888        // Wrong direction is an empty array.
889        assert_eq!(
890            evaluate("States.ArrayRange(5, 1, 1)", &Value::Null).unwrap(),
891            json!([])
892        );
893        assert!(evaluate("States.ArrayRange(1, 5, 0)", &Value::Null).is_err());
894    }
895
896    #[test]
897    fn nested_intrinsics() {
898        let input = json!({"s": "a,b,c", "x": 7});
899        assert_eq!(
900            evaluate(
901                "States.ArrayGetItem(States.StringSplit($.s, ','), 1)",
902                &input
903            )
904            .unwrap(),
905            json!("b")
906        );
907        assert_eq!(
908            evaluate(
909                r#"States.StringToJson(States.Format('\{"a":\{\}, "n": {}\}', $.x))"#,
910                &input
911            )
912            .unwrap(),
913            json!({"a": {}, "n": 7})
914        );
915        assert_eq!(
916            evaluate(
917                "States.ArrayLength(States.Array(States.MathAdd($.x, 1), 'p,q', States.Array()))",
918                &input
919            )
920            .unwrap(),
921            json!(3)
922        );
923    }
924
925    #[test]
926    fn nesting_limit_is_ten() {
927        let nest = |n: usize| {
928            let mut e = "States.Array()".to_string();
929            for _ in 1..n {
930                e = format!("States.Array({e})");
931            }
932            e
933        };
934        assert!(evaluate(&nest(10), &Value::Null).is_ok());
935        let err = evaluate(&nest(11), &Value::Null).unwrap_err();
936        assert!(format!("{err}").contains("nested"), "{err}");
937    }
938
939    #[test]
940    fn string_literal_escapes() {
941        assert_eq!(
942            evaluate(r"States.Format('it\'s \\ a \{\} {}', 'x')", &Value::Null).unwrap(),
943            json!(r"it's \ a {} x")
944        );
945        // Commas and parens inside strings never split arguments.
946        assert_eq!(
947            evaluate("States.Array('a,b', '(c)')", &Value::Null).unwrap(),
948            json!(["a,b", "(c)"])
949        );
950        // Escaped braces in a non-Format literal are plain braces.
951        assert_eq!(
952            evaluate(r"States.StringToJson('\{\}')", &Value::Null).unwrap(),
953            json!({})
954        );
955    }
956
957    #[test]
958    fn path_arguments_with_brackets_and_filters() {
959        let input = json!({"items": [{"id": 1, "n": "a"}, {"id": 2, "n": "b"}], "k": {"x,y": 3}});
960        assert_eq!(
961            evaluate("States.ArrayLength($.items[?(@.id > 1)])", &input).unwrap(),
962            json!(1)
963        );
964        assert_eq!(
965            evaluate("States.Array($['k']['x,y'], $.items[-1].n)", &input).unwrap(),
966            json!([3, "b"])
967        );
968    }
969
970    #[test]
971    fn missing_path_argument_is_runtime_error() {
972        let call = "States.Format('{}', $.nope)";
973        let err = evaluate_with_context(call, &json!({}), None)
974            .unwrap_err()
975            .into_states_error(call);
976        assert_eq!(err.0, "States.Runtime");
977        let call = "States.ArrayGetItem($.a, -1)";
978        let err = evaluate_with_context(call, &json!({"a": [1]}), None)
979            .unwrap_err()
980            .into_states_error(call);
981        assert_eq!(err.0, "States.Runtime");
982        assert!(err.1.starts_with(
983            "There was an error while evaluating the intrinsic function: \
984             States.ArrayGetItem($.a, -1). "
985        ));
986    }
987
988    /// Recorded: `States.Base64Decode('%%%')` is the empty string.
989    #[test]
990    fn base64_decode_skips_characters_outside_the_alphabet() {
991        assert_eq!(
992            evaluate("States.Base64Decode($.s)", &json!({"s": "%%%"})).unwrap(),
993            json!("")
994        );
995        assert_eq!(
996            evaluate("States.Base64Decode($.s)", &json!({"s": "aGk="})).unwrap(),
997            json!("hi")
998        );
999    }
1000
1001    /// Recorded: the causes of failing functions.
1002    #[test]
1003    fn failing_functions_read_as_recorded() {
1004        let cause = |call: &str, input: Value| {
1005            evaluate_with_context(call, &input, None)
1006                .unwrap_err()
1007                .into_states_error(call)
1008                .1
1009        };
1010        assert_eq!(
1011            cause("States.MathAdd($.s, 1)", json!({"s": "x"})),
1012            "There was an error while evaluating the intrinsic function: States.MathAdd($.s, 1). \
1013             Invalid arguments in States.MathAdd"
1014        );
1015        assert_eq!(
1016            cause("States.Format('{} {}', $.a)", json!({"a": "1"})),
1017            "There was an error while evaluating the intrinsic function: States.Format('{} {}', \
1018             $.a). Invalid arguments in States.Format: number of arguments do not match the \
1019             occurrences of {}."
1020        );
1021        assert_eq!(
1022            cause("States.ArrayRange(1, 2, 0)", json!({})),
1023            "There was an error while evaluating the intrinsic function: States.ArrayRange(1, 2, \
1024             0). Invalid arguments in States.ArrayRange"
1025        );
1026        assert!(cause("States.StringToJson($.x)", json!({"x": "not json"})).starts_with(
1027            "There was an error while evaluating the intrinsic function: States.StringToJson($.x). \
1028             Cannot convert String to Json in States.StringToJson, caused by: "
1029        ));
1030    }
1031
1032    #[test]
1033    fn context_path_arguments() {
1034        let ctx = json!({"Execution": {"Id": "arn:x"}});
1035        assert_eq!(
1036            evaluate_with_context(
1037                "States.Format('id={}', $$.Execution.Id)",
1038                &json!({}),
1039                Some(&ctx)
1040            )
1041            .unwrap(),
1042            json!("id=arn:x")
1043        );
1044    }
1045
1046    #[test]
1047    fn malformed_calls_error() {
1048        for bad in [
1049            "States.Format('{}'",
1050            "States.Array(1,",
1051            "States.Array(1) trailing",
1052            "States.Array('unterminated)",
1053            "States.Array(bogus)",
1054        ] {
1055            assert!(evaluate(bad, &Value::Null).is_err(), "{bad}");
1056        }
1057    }
1058}