Skip to main content

rite_runtime/
expressions.rs

1//! Expression evaluation for computed values.
2//!
3//! Evaluates pipeline expressions like `${artifact.ksr | sha256 | hex}` at runtime,
4//! resolving artifact references and applying transformations.
5
6use crate::actions::ArtifactValue;
7use crate::artifact_resolver::resolve_artifact_bytes;
8use crate::executor::ExecutionError;
9use crate::state::HandlerContext;
10use base16ct::lower::encode_string as hex_encode_string;
11use base32ct::{Base32Unpadded, Encoding as Base32Encoding};
12use base64ct::{Base64, Encoding as Base64Encoding};
13use rite_model::expression::{
14    ExprValue, Expression, Literal, PipeStage, RefType, Reference, StringPart, Value,
15    literal_to_json,
16};
17use rite_model::{ArtifactId, ParamId};
18use sha2::{Digest, Sha256, Sha384, Sha512};
19
20/// Evaluate an expression in the given context.
21///
22/// # Examples
23///
24/// ```text
25/// let expr = parse_expression("${artifact.ksr | sha256 | hex}")?;
26/// let result = evaluate(&expr, &context)?;
27/// // result is Value::String("<hex digest>")
28/// ```
29pub fn evaluate(expr: &Expression, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
30    match expr {
31        Expression::Reference(ref_) => evaluate_reference(ref_, ctx),
32        Expression::Literal(lit) => Ok(evaluate_literal(lit)),
33        Expression::Pipeline { source, stages } => {
34            let mut value = evaluate(source, ctx)?;
35            for stage in stages {
36                value = apply_pipe_stage(&value, stage, ctx)?;
37            }
38            Ok(value)
39        }
40    }
41}
42
43/// Evaluate a reference to get its value.
44fn evaluate_reference(ref_: &Reference, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
45    match ref_.ref_type {
46        RefType::Param => evaluate_param_ref(ref_, ctx),
47        RefType::Artifact => evaluate_artifact_ref(ref_, ctx),
48        // Role references evaluate to their ID as a string
49        // This allows ${role.name} to be used in role-related fields
50        RefType::Role => Ok(Value::String(ref_.name.clone())),
51    }
52}
53
54/// Evaluate a parameter reference.
55fn evaluate_param_ref(ref_: &Reference, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
56    let param_id = ParamId::new(&ref_.name);
57    let value = ctx.params.get(&param_id).ok_or_else(|| {
58        ExecutionError::InvalidParams(format!("Unknown parameter: {}", ref_.name))
59    })?;
60
61    json_to_value(value)
62}
63
64/// Convert a JSON value to our Value type.
65fn json_to_value(json: &serde_json::Value) -> Result<Value, ExecutionError> {
66    match json {
67        serde_json::Value::String(s) => Ok(Value::String(s.clone())),
68        serde_json::Value::Number(n) => {
69            if let Some(i) = n.as_i64() {
70                Ok(Value::Integer(i))
71            } else if let Some(f) = n.as_f64() {
72                #[allow(clippy::cast_possible_truncation)]
73                Ok(Value::Integer(f as i64))
74            } else {
75                Err(ExecutionError::InvalidParams(
76                    "Number too large".to_string(),
77                ))
78            }
79        }
80        serde_json::Value::Bool(b) => Ok(Value::Boolean(*b)),
81        serde_json::Value::Null => Ok(Value::Null),
82        serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
83            Err(ExecutionError::InvalidParams(
84                "Cannot use array or object as expression value".to_string(),
85            ))
86        }
87    }
88}
89
90/// Evaluate an artifact reference.
91fn evaluate_artifact_ref(ref_: &Reference, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
92    let artifact_id = ArtifactId::new(&ref_.name);
93    let artifact = ctx.get_artifact(&artifact_id).ok_or_else(|| {
94        ExecutionError::InvalidParams(format!("Artifact '{}' not found", ref_.name))
95    })?;
96
97    match artifact {
98        // Text references (physical items) - return as string for display
99        ArtifactValue::Text(text) => {
100            if let Some(property) = &ref_.property {
101                return Err(ExecutionError::InvalidParams(format!(
102                    "Property access '.{property}' not supported for text artifacts"
103                )));
104            }
105            Ok(Value::String(text.clone()))
106        }
107
108        // Binary content (documents, crypto materials) and opened content:
109        // returned as bytes. The value is a copy, since an expression owns
110        // what it computes on, and the copy is wiped with the value.
111        ArtifactValue::Bytes(_) | ArtifactValue::Secret(_) => {
112            // Property access not supported for raw byte artifacts
113            if let Some(property) = &ref_.property {
114                return Err(ExecutionError::InvalidParams(format!(
115                    "Property access '.{property}' not supported for binary artifacts"
116                )));
117            }
118            let bytes = resolve_artifact_bytes(ctx.artifacts, &artifact_id, None)?;
119            Ok(Value::bytes(bytes.to_vec()))
120        }
121
122        // A share never becomes an expression value. The value would be
123        // copied into `with:` as JSON, or into a prompt text that the
124        // transcript records; a step that shows or combines a share takes it
125        // through `reads:` and holds a borrow instead.
126        ArtifactValue::Shares(_) => Err(ExecutionError::InvalidParams(format!(
127            "'{artifact_id}' holds shares of a secret, which no expression renders; \
128             name the share under 'reads:' of a step that shows or combines it"
129        ))),
130
131        // Other artifact types - delegate to resolve_artifact_bytes
132        _ => {
133            let bytes =
134                resolve_artifact_bytes(ctx.artifacts, &artifact_id, ref_.property.as_deref())?;
135            Ok(Value::bytes(bytes.to_vec()))
136        }
137    }
138}
139
140/// Evaluate a literal to a Value.
141fn evaluate_literal(lit: &Literal) -> Value {
142    match lit {
143        Literal::String(s) => Value::String(s.clone()),
144        Literal::Integer(i) => Value::Integer(*i),
145        #[allow(clippy::cast_possible_truncation)]
146        Literal::Float(f) => Value::Integer(*f as i64), // Truncate float to integer
147        Literal::Boolean(b) => Value::Boolean(*b),
148        Literal::Null => Value::Null,
149    }
150}
151
152/// Apply a pipe stage to a value.
153fn apply_pipe_stage(
154    input: &Value,
155    stage: &PipeStage,
156    ctx: &HandlerContext,
157) -> Result<Value, ExecutionError> {
158    match stage.function.as_str() {
159        // Hash functions: Bytes -> Bytes
160        "sha256" => apply_sha256(input),
161        "sha384" => apply_sha384(input),
162        "sha512" => apply_sha512(input),
163
164        // Encoding functions: Bytes -> String
165        "hex" => apply_hex(input),
166        "base32" => apply_base32(input),
167        "base64" => apply_base64(input),
168
169        // String functions: String -> String
170        "upper" => apply_upper(input),
171        "lower" => apply_lower(input),
172        "substr" => apply_substr(input, &stage.args, ctx),
173
174        // Bytes concatenation
175        "concat" => apply_concat(&stage.args, ctx),
176
177        unknown => Err(ExecutionError::InvalidParams(format!(
178            "Unknown function: {unknown}"
179        ))),
180    }
181}
182
183fn apply_sha256(input: &Value) -> Result<Value, ExecutionError> {
184    let bytes = input.as_bytes().ok_or_else(|| {
185        ExecutionError::InvalidParams(format!(
186            "sha256 expects bytes or string, got {}",
187            input.type_name()
188        ))
189    })?;
190
191    let mut hasher = Sha256::new();
192    hasher.update(bytes);
193    let result = hasher.finalize();
194
195    Ok(Value::bytes(result.to_vec()))
196}
197
198fn apply_sha384(input: &Value) -> Result<Value, ExecutionError> {
199    let bytes = input.as_bytes().ok_or_else(|| {
200        ExecutionError::InvalidParams(format!(
201            "sha384 expects bytes or string, got {}",
202            input.type_name()
203        ))
204    })?;
205
206    let mut hasher = Sha384::new();
207    hasher.update(bytes);
208    let result = hasher.finalize();
209
210    Ok(Value::bytes(result.to_vec()))
211}
212
213fn apply_sha512(input: &Value) -> Result<Value, ExecutionError> {
214    let bytes = input.as_bytes().ok_or_else(|| {
215        ExecutionError::InvalidParams(format!(
216            "sha512 expects bytes or string, got {}",
217            input.type_name()
218        ))
219    })?;
220
221    let mut hasher = Sha512::new();
222    hasher.update(bytes);
223    let result = hasher.finalize();
224
225    Ok(Value::bytes(result.to_vec()))
226}
227
228fn apply_hex(input: &Value) -> Result<Value, ExecutionError> {
229    let bytes = input.as_bytes().ok_or_else(|| {
230        ExecutionError::InvalidParams(format!(
231            "hex expects bytes or string, got {}",
232            input.type_name()
233        ))
234    })?;
235
236    let hex_string = hex_encode(bytes);
237    Ok(Value::String(hex_string))
238}
239
240fn apply_base32(input: &Value) -> Result<Value, ExecutionError> {
241    let bytes = input.as_bytes().ok_or_else(|| {
242        ExecutionError::InvalidParams(format!(
243            "base32 expects bytes or string, got {}",
244            input.type_name()
245        ))
246    })?;
247
248    // base32ct returns lowercase, but RFC 4648 specifies uppercase
249    let encoded = Base32Unpadded::encode_string(bytes).to_uppercase();
250    Ok(Value::String(encoded))
251}
252
253fn apply_base64(input: &Value) -> Result<Value, ExecutionError> {
254    let bytes = input.as_bytes().ok_or_else(|| {
255        ExecutionError::InvalidParams(format!(
256            "base64 expects bytes or string, got {}",
257            input.type_name()
258        ))
259    })?;
260
261    let encoded = Base64::encode_string(bytes);
262    Ok(Value::String(encoded))
263}
264
265/// Encode bytes as lowercase hex string using constant-time encoding.
266fn hex_encode(bytes: &[u8]) -> String {
267    hex_encode_string(bytes)
268}
269
270fn apply_upper(input: &Value) -> Result<Value, ExecutionError> {
271    let s = input.as_string().ok_or_else(|| {
272        ExecutionError::InvalidParams(format!("upper expects string, got {}", input.type_name()))
273    })?;
274
275    Ok(Value::String(s.to_uppercase()))
276}
277
278fn apply_lower(input: &Value) -> Result<Value, ExecutionError> {
279    let s = input.as_string().ok_or_else(|| {
280        ExecutionError::InvalidParams(format!("lower expects string, got {}", input.type_name()))
281    })?;
282
283    Ok(Value::String(s.to_lowercase()))
284}
285
286fn apply_substr(
287    input: &Value,
288    args: &[Expression],
289    ctx: &HandlerContext,
290) -> Result<Value, ExecutionError> {
291    let s = input.as_string().ok_or_else(|| {
292        ExecutionError::InvalidParams(format!("substr expects string, got {}", input.type_name()))
293    })?;
294
295    let [start_arg, len_arg] = args else {
296        return Err(ExecutionError::InvalidParams(
297            "substr requires 2 arguments: start and length".to_string(),
298        ));
299    };
300
301    let start_i64 = evaluate(start_arg, ctx)?
302        .as_integer()
303        .ok_or_else(|| ExecutionError::InvalidParams("substr start must be integer".to_string()))?;
304
305    let len_i64 = evaluate(len_arg, ctx)?.as_integer().ok_or_else(|| {
306        ExecutionError::InvalidParams("substr length must be integer".to_string())
307    })?;
308
309    // Clamp negative values to 0, then convert to usize
310    let start = usize::try_from(start_i64.max(0)).unwrap_or(0).min(s.len());
311    let len = usize::try_from(len_i64.max(0)).unwrap_or(0);
312    let end = start.saturating_add(len).min(s.len());
313
314    Ok(Value::String(
315        s.get(start..end).unwrap_or_default().to_string(),
316    ))
317}
318
319fn apply_concat(args: &[Expression], ctx: &HandlerContext) -> Result<Value, ExecutionError> {
320    let values = args
321        .iter()
322        .map(|arg| evaluate(arg, ctx))
323        .collect::<Result<Vec<_>, _>>()?;
324    let parts = values
325        .iter()
326        .map(|value| {
327            value.as_bytes().ok_or_else(|| {
328                ExecutionError::InvalidParams(format!(
329                    "concat expects bytes arguments, got {}",
330                    value.type_name()
331                ))
332            })
333        })
334        .collect::<Result<Vec<_>, _>>()?;
335
336    // Sized up front. A buffer that grows as it goes leaves each outgrown
337    // allocation behind unwiped, with a prefix of the content in it.
338    let mut result = Vec::with_capacity(parts.iter().map(|part| part.len()).sum());
339    for part in parts {
340        result.extend_from_slice(part);
341    }
342
343    Ok(Value::bytes(result))
344}
345
346/// Evaluate an `ExprValue` to a JSON value.
347///
348/// The function receives pre-parsed expressions and performs NO string
349/// parsing. All `${...}` patterns have already been parsed by the resolver.
350///
351/// # Examples
352///
353/// ```text
354/// let expr_value = parse_expr_value(&json!({"message": "${param.name}"}));
355/// let result = evaluate_expr_value(&expr_value, &ctx)?;
356/// // result is a serde_json::Value with expressions evaluated
357/// ```
358pub fn evaluate_expr_value(
359    value: &ExprValue,
360    ctx: &HandlerContext,
361) -> Result<serde_json::Value, ExecutionError> {
362    match value {
363        ExprValue::Literal(lit) => Ok(literal_to_json(lit)),
364        ExprValue::Expr(expr) => {
365            let result = evaluate(expr, ctx)?;
366            Ok(value_to_json(&result))
367        }
368        ExprValue::Interpolated(parts) => {
369            let mut result = String::new();
370            for part in parts {
371                match part {
372                    StringPart::Literal(s) => result.push_str(s),
373                    StringPart::Expr(expr) => {
374                        let val = evaluate(expr, ctx)?;
375                        result.push_str(&val.to_string());
376                    }
377                }
378            }
379            Ok(serde_json::Value::String(result))
380        }
381        ExprValue::Object(map) => {
382            let mut result = serde_json::Map::new();
383            for (k, v) in map {
384                result.insert(k.clone(), evaluate_expr_value(v, ctx)?);
385            }
386            Ok(serde_json::Value::Object(result))
387        }
388        ExprValue::Array(arr) => {
389            let result: Result<Vec<_>, _> =
390                arr.iter().map(|v| evaluate_expr_value(v, ctx)).collect();
391            Ok(serde_json::Value::Array(result?))
392        }
393    }
394}
395
396/// Evaluate an `ExprValue` to a string.
397///
398/// Useful for description fields that should always produce a string.
399pub fn evaluate_expr_value_to_string(
400    value: &ExprValue,
401    ctx: &HandlerContext,
402) -> Result<String, ExecutionError> {
403    match value {
404        ExprValue::Literal(Literal::String(s)) => Ok(s.clone()),
405        ExprValue::Literal(lit) => {
406            // Convert other literals to string representation
407            let json = literal_to_json(lit);
408            Ok(match json {
409                serde_json::Value::String(s) => s,
410                serde_json::Value::Number(n) => n.to_string(),
411                serde_json::Value::Bool(b) => b.to_string(),
412                serde_json::Value::Null => "null".to_string(),
413                _ => json.to_string(),
414            })
415        }
416        ExprValue::Expr(expr) => {
417            let result = evaluate(expr, ctx)?;
418            Ok(result.to_string())
419        }
420        ExprValue::Interpolated(parts) => {
421            let mut result = String::new();
422            for part in parts {
423                match part {
424                    StringPart::Literal(s) => result.push_str(s),
425                    StringPart::Expr(expr) => {
426                        let val = evaluate(expr, ctx)?;
427                        result.push_str(&val.to_string());
428                    }
429                }
430            }
431            Ok(result)
432        }
433        ExprValue::Object(_) | ExprValue::Array(_) => {
434            // For complex types, return JSON string representation
435            let json = evaluate_expr_value(value, ctx)?;
436            Ok(json.to_string())
437        }
438    }
439}
440
441/// Convert a Value to a JSON value for action parameters.
442pub fn value_to_json(value: &Value) -> serde_json::Value {
443    match value {
444        Value::Bytes(b) => {
445            // Bytes are encoded as base64 for JSON
446            serde_json::Value::String(Base64::encode_string(b))
447        }
448        Value::String(s) => serde_json::Value::String(s.clone()),
449        Value::Integer(i) => serde_json::Value::Number((*i).into()),
450        Value::Boolean(b) => serde_json::Value::Bool(*b),
451        Value::Null => serde_json::Value::Null,
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::actions::ArtifactValue;
459    use rite_model::{MaterialId, RoleId};
460    use std::collections::HashMap;
461
462    fn empty_context() -> HandlerContext<'static> {
463        static EMPTY_PARAMS: std::sync::LazyLock<HashMap<ParamId, serde_json::Value>> =
464            std::sync::LazyLock::new(HashMap::new);
465        static EMPTY_ARTIFACTS: std::sync::LazyLock<HashMap<ArtifactId, ArtifactValue>> =
466            std::sync::LazyLock::new(HashMap::new);
467        static EMPTY_ROLES: std::sync::LazyLock<HashMap<RoleId, String>> =
468            std::sync::LazyLock::new(HashMap::new);
469        static EMPTY_MATERIALS: std::sync::LazyLock<HashMap<MaterialId, String>> =
470            std::sync::LazyLock::new(HashMap::new);
471        HandlerContext {
472            params: &EMPTY_PARAMS,
473            artifacts: &EMPTY_ARTIFACTS,
474            roles: &EMPTY_ROLES,
475            materials: &EMPTY_MATERIALS,
476        }
477    }
478
479    /// Create a `HandlerContext` from params and artifacts for tests.
480    /// Note: This leaks memory, only use in tests.
481    fn make_context(
482        params: HashMap<ParamId, serde_json::Value>,
483        artifacts: HashMap<ArtifactId, ArtifactValue>,
484    ) -> HandlerContext<'static> {
485        static EMPTY_ROLES: std::sync::LazyLock<HashMap<RoleId, String>> =
486            std::sync::LazyLock::new(HashMap::new);
487        static EMPTY_MATERIALS: std::sync::LazyLock<HashMap<MaterialId, String>> =
488            std::sync::LazyLock::new(HashMap::new);
489        let params_box = Box::leak(Box::new(params));
490        let artifacts_box = Box::leak(Box::new(artifacts));
491        HandlerContext {
492            params: params_box,
493            artifacts: artifacts_box,
494            roles: &EMPTY_ROLES,
495            materials: &EMPTY_MATERIALS,
496        }
497    }
498
499    /// A share reaches a step only through `reads:`; an expression would
500    /// copy it into `with:` or a prompt that the transcript records.
501    #[test]
502    fn a_share_set_is_refused_by_every_expression_form() {
503        let share = crate::Share::new(2, 1, vec![0xAB; 4]);
504        let mut artifacts: HashMap<ArtifactId, ArtifactValue> = HashMap::new();
505        artifacts.insert(
506            ArtifactId::new("shares"),
507            ArtifactValue::Shares(crate::ShareSet::new(2, [share])),
508        );
509        let ctx = make_context(HashMap::new(), artifacts);
510
511        for text in [
512            "${artifact.shares}",
513            "${artifact.shares.share_1}",
514            "${artifact.shares | hex}",
515        ] {
516            let expr = rite_model::expression::parse_expression(text).unwrap();
517            let error = evaluate(&expr, &ctx).expect_err(text).to_string();
518            assert!(error.contains("no expression renders"), "{text}: {error}");
519            assert!(!error.contains("ab"), "{text}: {error}");
520        }
521    }
522
523    #[test]
524    fn sha256_of_a_string_matches_the_known_digest() {
525        let input = Value::String("hello".to_string());
526        let result = apply_sha256(&input).unwrap();
527
528        if let Value::Bytes(bytes) = result {
529            assert_eq!(bytes.len(), 32); // SHA-256 produces 32 bytes
530            let hex = hex_encode(&bytes);
531            assert_eq!(
532                hex,
533                "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
534            );
535        } else {
536            panic!("Expected Bytes");
537        }
538    }
539
540    #[test]
541    fn sha256_of_bytes_produces_32_bytes() {
542        let input = Value::bytes(b"hello".to_vec());
543        let result = apply_sha256(&input).unwrap();
544
545        if let Value::Bytes(bytes) = result {
546            assert_eq!(bytes.len(), 32);
547        } else {
548            panic!("Expected Bytes");
549        }
550    }
551
552    #[test]
553    fn hex_encodes_bytes_in_lowercase() {
554        let input = Value::bytes(vec![0xde, 0xad, 0xbe, 0xef]);
555        let result = apply_hex(&input).unwrap();
556
557        assert_eq!(result, Value::String("deadbeef".to_string()));
558    }
559
560    #[test]
561    fn base32_encodes_bytes_unpadded_and_uppercase() {
562        let input = Value::bytes(b"hello".to_vec());
563        let result = apply_base32(&input).unwrap();
564
565        // Base32 unpadded encoding of "hello" (RFC 4648 uppercase)
566        assert_eq!(result, Value::String("NBSWY3DP".to_string()));
567    }
568
569    #[test]
570    fn base64_encodes_bytes_with_padding() {
571        let input = Value::bytes(b"hello".to_vec());
572        let result = apply_base64(&input).unwrap();
573
574        assert_eq!(result, Value::String("aGVsbG8=".to_string()));
575    }
576
577    #[test]
578    fn upper_uppercases_a_string() {
579        let input = Value::String("hello".to_string());
580        let result = apply_upper(&input).unwrap();
581
582        assert_eq!(result, Value::String("HELLO".to_string()));
583    }
584
585    #[test]
586    fn lower_lowercases_a_string() {
587        let input = Value::String("HELLO".to_string());
588        let result = apply_lower(&input).unwrap();
589
590        assert_eq!(result, Value::String("hello".to_string()));
591    }
592
593    #[test]
594    fn a_pipeline_chains_sha256_into_hex() {
595        let ctx = empty_context();
596
597        // ${"hello" | sha256 | hex}
598        let expr = Expression::Pipeline {
599            source: Box::new(Expression::Literal(Literal::String("hello".to_string()))),
600            stages: vec![PipeStage::new("sha256"), PipeStage::new("hex")],
601        };
602
603        let result = evaluate(&expr, &ctx).unwrap();
604
605        if let Value::String(s) = result {
606            assert_eq!(
607                s,
608                "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
609            );
610        } else {
611            panic!("Expected String");
612        }
613    }
614
615    #[test]
616    fn a_pipeline_chains_sha256_hex_and_upper() {
617        let ctx = empty_context();
618
619        // ${"hello" | sha256 | hex | upper}
620        let expr = Expression::Pipeline {
621            source: Box::new(Expression::Literal(Literal::String("hello".to_string()))),
622            stages: vec![
623                PipeStage::new("sha256"),
624                PipeStage::new("hex"),
625                PipeStage::new("upper"),
626            ],
627        };
628
629        let result = evaluate(&expr, &ctx).unwrap();
630
631        if let Value::String(s) = result {
632            assert_eq!(
633                s,
634                "2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824"
635            );
636        } else {
637            panic!("Expected String");
638        }
639    }
640
641    #[test]
642    fn sha256_rejects_an_integer() {
643        let input = Value::Integer(42);
644        let result = apply_sha256(&input);
645
646        assert!(result.is_err());
647    }
648
649    #[test]
650    fn upper_rejects_bytes() {
651        let input = Value::bytes(vec![1, 2, 3]);
652        let result = apply_upper(&input);
653
654        assert!(result.is_err());
655    }
656
657    #[test]
658    fn substr_takes_a_prefix() {
659        let ctx = empty_context();
660
661        // ${"abcdefgh" | substr(0, 4)}
662        let expr = Expression::Pipeline {
663            source: Box::new(Expression::Literal(Literal::String("abcdefgh".to_string()))),
664            stages: vec![PipeStage {
665                function: "substr".to_string(),
666                args: vec![
667                    Expression::Literal(Literal::Integer(0)),
668                    Expression::Literal(Literal::Integer(4)),
669                ],
670            }],
671        };
672
673        let result = evaluate(&expr, &ctx).unwrap();
674        assert_eq!(result, Value::String("abcd".to_string()));
675    }
676
677    #[test]
678    fn substr_takes_a_range_from_the_middle() {
679        let ctx = empty_context();
680
681        // ${"abcdefgh" | substr(2, 4)}
682        let expr = Expression::Pipeline {
683            source: Box::new(Expression::Literal(Literal::String("abcdefgh".to_string()))),
684            stages: vec![PipeStage {
685                function: "substr".to_string(),
686                args: vec![
687                    Expression::Literal(Literal::Integer(2)),
688                    Expression::Literal(Literal::Integer(4)),
689                ],
690            }],
691        };
692
693        let result = evaluate(&expr, &ctx).unwrap();
694        assert_eq!(result, Value::String("cdef".to_string()));
695    }
696
697    #[test]
698    fn an_artifact_reference_resolves_through_a_pipeline() {
699        let mut params = HashMap::new();
700        params.insert(ParamId::new("name"), serde_json::json!("test"));
701
702        let mut artifacts = HashMap::new();
703        // Use Bytes which stores binary content
704        artifacts.insert(
705            ArtifactId::new("ksr"),
706            ArtifactValue::Bytes(b"KSR content".to_vec()),
707        );
708
709        let ctx = make_context(params, artifacts);
710
711        // Parse and evaluate: ${artifact.ksr | sha256 | hex}
712        let expr =
713            rite_model::expression::parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
714        let result = evaluate(&expr, &ctx).unwrap();
715
716        if let Value::String(s) = result {
717            assert_eq!(s.len(), 64); // SHA-256 hex is 64 chars
718        } else {
719            panic!("Expected String");
720        }
721    }
722
723    #[test]
724    fn a_param_reference_resolves_to_its_value() {
725        let mut params = HashMap::new();
726        params.insert(ParamId::new("expected_hash"), serde_json::json!("abc123"));
727
728        let artifacts = HashMap::new();
729        let ctx = make_context(params, artifacts);
730
731        let expr = rite_model::expression::parse_expression("${param.expected_hash}").unwrap();
732        let result = evaluate(&expr, &ctx).unwrap();
733
734        assert_eq!(result, Value::String("abc123".to_string()));
735    }
736
737    #[test]
738    fn sha384_produces_48_bytes() {
739        let input = Value::String("hello".to_string());
740        let result = apply_sha384(&input).unwrap();
741
742        if let Value::Bytes(bytes) = result {
743            assert_eq!(bytes.len(), 48); // SHA-384 produces 48 bytes
744        } else {
745            panic!("Expected Bytes");
746        }
747    }
748
749    #[test]
750    fn sha512_produces_64_bytes() {
751        let input = Value::String("hello".to_string());
752        let result = apply_sha512(&input).unwrap();
753
754        if let Value::Bytes(bytes) = result {
755            assert_eq!(bytes.len(), 64); // SHA-512 produces 64 bytes
756        } else {
757            panic!("Expected Bytes");
758        }
759    }
760
761    #[test]
762    fn concat_joins_its_arguments_and_ignores_the_input() {
763        let ctx = empty_context();
764
765        // ${"ignored" | concat("hello", " ", "world")}, which takes its arguments
766        // as the whole input and ignores what the pipeline hands it.
767        let expr = Expression::Pipeline {
768            source: Box::new(Expression::Literal(Literal::String("ignored".to_string()))),
769            stages: vec![PipeStage {
770                function: "concat".to_string(),
771                args: vec![
772                    Expression::Literal(Literal::String("hello".to_string())),
773                    Expression::Literal(Literal::String(" ".to_string())),
774                    Expression::Literal(Literal::String("world".to_string())),
775                ],
776            }],
777        };
778
779        let result = evaluate(&expr, &ctx).unwrap();
780
781        // concat returns bytes
782        if let Value::Bytes(b) = result {
783            assert_eq!(b.as_slice(), b"hello world");
784        } else {
785            panic!("Expected Bytes, got {}", result.type_name());
786        }
787    }
788
789    #[test]
790    fn a_parsed_expression_string_evaluates_end_to_end() {
791        let ctx = empty_context();
792
793        let expr = rite_model::expression::parse_expression("${\"test\" | sha256 | hex}").unwrap();
794        let result = evaluate(&expr, &ctx).unwrap();
795
796        if let Value::String(s) = result {
797            // SHA-256 of "test" in hex
798            assert_eq!(
799                s,
800                "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
801            );
802        } else {
803            panic!("Expected String");
804        }
805    }
806
807    mod ceremony_examples {
808        use super::*;
809
810        fn ceremony_context() -> HandlerContext<'static> {
811            let params: HashMap<ParamId, serde_json::Value> = [
812                ("ceremony_id", serde_json::json!("KC-2024-Q4")),
813                ("hsm_serial", serde_json::json!("HSM-001")),
814                ("key_label", serde_json::json!("MASTER-KEY")),
815                ("expected_ksr_hash", serde_json::json!("a3b8f4e91c2d6078")),
816                ("zone_name", serde_json::json!(".")),
817                ("signing_validity_days", serde_json::json!(21)),
818            ]
819            .into_iter()
820            .map(|(k, v)| (ParamId::new(k), v))
821            .collect();
822
823            let mut artifacts: HashMap<ArtifactId, ArtifactValue> = HashMap::new();
824            artifacts.insert(
825                ArtifactId::new("ksr"),
826                ArtifactValue::Bytes(b"KSR test content".to_vec()),
827            );
828            artifacts.insert(
829                ArtifactId::new("master_public_key"),
830                ArtifactValue::Bytes(b"PUBLIC KEY DATA".to_vec()),
831            );
832
833            make_context(params, artifacts)
834        }
835
836        // From dnssec_signing/dnssec_signing.rite.yaml
837        #[test]
838        fn dnssec_hash_verification_full_pipeline() {
839            let ctx = ceremony_context();
840
841            // actual: "${artifact.ksr | sha256 | hex | substr(0, 16)}"
842            let expr = rite_model::expression::parse_expression(
843                "${artifact.ksr | sha256 | hex | substr(0, 16)}",
844            )
845            .unwrap();
846            let result = evaluate(&expr, &ctx).unwrap();
847
848            if let Value::String(s) = result {
849                assert_eq!(s.len(), 16); // First 16 hex chars
850                assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
851            } else {
852                panic!("Expected String");
853            }
854        }
855
856        #[test]
857        fn dnssec_param_comparison() {
858            let ctx = ceremony_context();
859
860            // expected: "${param.expected_ksr_hash}"
861            let expr =
862                rite_model::expression::parse_expression("${param.expected_ksr_hash}").unwrap();
863            let result = evaluate(&expr, &ctx).unwrap();
864
865            assert_eq!(result, Value::String("a3b8f4e91c2d6078".to_string()));
866        }
867
868        #[test]
869        fn hsm_fingerprint_computation() {
870            let ctx = ceremony_context();
871
872            // value: "${artifact.master_public_key | sha256 | hex}"
873            let expr = rite_model::expression::parse_expression(
874                "${artifact.master_public_key | sha256 | hex}",
875            )
876            .unwrap();
877            let result = evaluate(&expr, &ctx).unwrap();
878
879            if let Value::String(s) = result {
880                assert_eq!(s.len(), 64); // Full SHA-256 hex
881            } else {
882                panic!("Expected String");
883            }
884        }
885    }
886}