1#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
30
31use std::ops::Range;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum RefType {
39 Param,
41 Role,
43 Artifact,
45}
46
47impl std::str::FromStr for RefType {
48 type Err = String;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 match s {
52 "param" => Ok(Self::Param),
53 "role" => Ok(Self::Role),
54 "artifact" => Ok(Self::Artifact),
55 _ => Err(format!("unknown reference type: {s}")),
56 }
57 }
58}
59
60impl std::fmt::Display for RefType {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Param => write!(f, "param"),
64 Self::Role => write!(f, "role"),
65 Self::Artifact => write!(f, "artifact"),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub enum Value {
73 Bytes(Vec<u8>),
75 String(String),
77 Integer(i64),
79 Boolean(bool),
81 Null,
83}
84
85impl Value {
86 pub fn as_bytes(&self) -> Option<Vec<u8>> {
89 match self {
90 Value::Bytes(b) => Some(b.clone()),
91 Value::String(s) => Some(s.as_bytes().to_vec()),
92 _ => None,
93 }
94 }
95
96 pub fn as_string(&self) -> Option<&str> {
98 match self {
99 Value::String(s) => Some(s),
100 _ => None,
101 }
102 }
103
104 pub fn as_integer(&self) -> Option<i64> {
106 match self {
107 Value::Integer(i) => Some(*i),
108 _ => None,
109 }
110 }
111
112 pub fn is_null(&self) -> bool {
114 matches!(self, Value::Null)
115 }
116
117 pub fn type_name(&self) -> &'static str {
119 match self {
120 Value::Bytes(_) => "bytes",
121 Value::String(_) => "string",
122 Value::Integer(_) => "integer",
123 Value::Boolean(_) => "boolean",
124 Value::Null => "null",
125 }
126 }
127}
128
129impl std::fmt::Display for Value {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
133 Value::String(s) => write!(f, "{s}"),
134 Value::Integer(i) => write!(f, "{i}"),
135 Value::Boolean(b) => write!(f, "{b}"),
136 Value::Null => write!(f, "null"),
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq)]
148pub enum Expression {
149 Reference(Reference),
151
152 Literal(Literal),
154
155 Pipeline {
157 source: Box<Expression>,
159 stages: Vec<PipeStage>,
161 },
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct Reference {
167 pub ref_type: RefType,
169 pub name: String,
171 pub property: Option<String>,
173}
174
175impl Reference {
176 pub fn new(ref_type: RefType, name: &str) -> Self {
178 Self {
179 ref_type,
180 name: name.to_string(),
181 property: None,
182 }
183 }
184
185 pub fn with_property(ref_type: RefType, name: &str, property: &str) -> Self {
187 Self {
188 ref_type,
189 name: name.to_string(),
190 property: Some(property.to_string()),
191 }
192 }
193}
194
195impl std::fmt::Display for Reference {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 if let Some(prop) = &self.property {
198 write!(f, "{}.{}.{}", self.ref_type, self.name, prop)
199 } else {
200 write!(f, "{}.{}", self.ref_type, self.name)
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq)]
207pub enum Literal {
208 String(String),
210 Integer(i64),
212 Float(f64),
214 Boolean(bool),
216 Null,
218}
219
220#[allow(clippy::cast_possible_truncation)]
221impl From<Literal> for Value {
222 fn from(lit: Literal) -> Self {
223 match lit {
224 Literal::String(s) => Value::String(s),
225 Literal::Integer(i) => Value::Integer(i),
226 Literal::Float(f) => Value::Integer(f as i64), Literal::Boolean(b) => Value::Boolean(b),
228 Literal::Null => Value::Null,
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub struct PipeStage {
236 pub function: String,
238 pub args: Vec<Expression>,
240}
241
242impl PipeStage {
243 pub fn new(function: &str) -> Self {
245 Self {
246 function: function.to_string(),
247 args: Vec::new(),
248 }
249 }
250
251 pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
253 Self {
254 function: function.to_string(),
255 args,
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
262pub struct LocatedExpression {
263 pub expression: Expression,
265 pub range: Range<usize>,
267}
268
269#[derive(Debug, Clone, PartialEq)]
275pub enum ExprValue {
276 Literal(Literal),
278 Expr(Expression),
280 Interpolated(Vec<StringPart>),
282 Object(std::collections::HashMap<String, ExprValue>),
284 Array(Vec<ExprValue>),
286}
287
288impl ExprValue {
289 pub fn as_literal_string(&self) -> Option<&str> {
291 match self {
292 ExprValue::Literal(Literal::String(s)) => Some(s),
293 _ => None,
294 }
295 }
296
297 pub fn get(&self, key: &str) -> Option<&ExprValue> {
299 match self {
300 ExprValue::Object(map) => map.get(key),
301 _ => None,
302 }
303 }
304
305 pub fn to_display_string(&self) -> String {
310 match self {
311 ExprValue::Literal(Literal::String(s)) => s.clone(),
312 ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
313 ExprValue::Literal(Literal::Float(f)) => f.to_string(),
314 ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
315 ExprValue::Literal(Literal::Null) => "null".to_string(),
316 ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
317 ExprValue::Interpolated(parts) => parts
318 .iter()
319 .map(|p| match p {
320 StringPart::Literal(s) => s.clone(),
321 StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
322 })
323 .collect(),
324 ExprValue::Object(_) => "[object]".to_string(),
325 ExprValue::Array(_) => "[array]".to_string(),
326 }
327 }
328}
329
330fn expr_to_string(expr: &Expression) -> String {
332 match expr {
333 Expression::Reference(r) => r.to_string(),
334 Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
335 Expression::Literal(Literal::Integer(i)) => i.to_string(),
336 Expression::Literal(Literal::Float(f)) => f.to_string(),
337 Expression::Literal(Literal::Boolean(b)) => b.to_string(),
338 Expression::Literal(Literal::Null) => "null".to_string(),
339 Expression::Pipeline { source, stages } => {
340 let mut result = expr_to_string(source);
341 for stage in stages {
342 result.push_str(" | ");
343 result.push_str(&stage.function);
344 if !stage.args.is_empty() {
345 result.push('(');
346 let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
347 result.push_str(&args.join(", "));
348 result.push(')');
349 }
350 }
351 result
352 }
353 }
354}
355
356#[derive(Debug, Clone, PartialEq)]
358pub enum StringPart {
359 Literal(String),
361 Expr(Expression),
363}
364
365pub fn parse_expression(s: &str) -> Option<Expression> {
387 let s = s.trim();
388
389 let inner = s.strip_prefix("${").and_then(|s| s.strip_suffix('}'))?;
391 let inner = inner.trim();
392
393 if inner.is_empty() {
394 return None;
395 }
396
397 parse_pipeline(inner)
398}
399
400fn parse_pipeline(s: &str) -> Option<Expression> {
402 let s = s.trim();
403
404 let parts = split_on_pipes(s);
406
407 if parts.is_empty() {
408 return None;
409 }
410
411 let source = parse_source(parts.first()?.trim())?;
413
414 if parts.len() == 1 {
415 return Some(source);
417 }
418
419 let mut stages = Vec::new();
421 for part in parts.iter().skip(1) {
422 let stage = parse_pipe_stage(part.trim())?;
423 stages.push(stage);
424 }
425
426 Some(Expression::Pipeline {
427 source: Box::new(source),
428 stages,
429 })
430}
431
432fn split_on_pipes(s: &str) -> Vec<&str> {
434 let mut parts = Vec::new();
435 let mut start = 0;
436 let mut paren_depth: u32 = 0;
437 let bytes = s.as_bytes();
438
439 for (i, &b) in bytes.iter().enumerate() {
440 match b {
441 b'(' => paren_depth += 1,
442 b')' => paren_depth = paren_depth.saturating_sub(1),
443 b'|' if paren_depth == 0 => {
444 parts.push(&s[start..i]);
445 start = i + 1;
446 }
447 _ => {}
448 }
449 }
450
451 if start < s.len() {
453 parts.push(&s[start..]);
454 }
455
456 parts
457}
458
459fn parse_source(s: &str) -> Option<Expression> {
461 let s = s.trim();
462
463 if let Some(expr) = parse_function_call(s) {
465 return Some(expr);
466 }
467
468 if let Some(expr_ref) = parse_ref(s) {
470 return Some(Expression::Reference(expr_ref));
471 }
472
473 if let Some(lit) = parse_literal(s) {
475 return Some(Expression::Literal(lit));
476 }
477
478 None
479}
480
481fn parse_ref(s: &str) -> Option<Reference> {
483 let parts: Vec<&str> = s.splitn(3, '.').collect();
484
485 if parts.len() < 2 {
486 return None;
487 }
488
489 let namespace = parts[0];
490 if !is_valid_identifier(namespace) {
491 return None;
492 }
493
494 let ref_type: RefType = namespace.parse().ok()?;
496
497 let name = parts[1];
498 if !is_valid_identifier(name) {
499 return None;
500 }
501
502 let property = if parts.len() == 3 {
503 let prop = parts[2];
504 if !is_valid_identifier(prop) {
505 return None;
506 }
507 Some(prop.to_string())
508 } else {
509 None
510 };
511
512 Some(Reference {
513 ref_type,
514 name: name.to_string(),
515 property,
516 })
517}
518
519fn parse_literal(s: &str) -> Option<Literal> {
521 let s = s.trim();
522
523 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
525 let inner = &s[1..s.len() - 1];
526 return Some(Literal::String(inner.to_string()));
527 }
528
529 if let Ok(i) = s.parse::<i64>() {
531 return Some(Literal::Integer(i));
532 }
533
534 None
535}
536
537fn parse_function_call(s: &str) -> Option<Expression> {
539 let s = s.trim();
540
541 let paren_pos = s.find('(')?;
543 let name = &s[..paren_pos];
544
545 if !is_valid_identifier(name) {
546 return None;
547 }
548
549 if !s.ends_with(')') {
551 return None;
552 }
553
554 let args_str = &s[paren_pos + 1..s.len() - 1];
555
556 let args = parse_function_args(args_str)?;
558
559 Some(Expression::Pipeline {
562 source: Box::new(Expression::Literal(Literal::Integer(0))), stages: vec![PipeStage::with_args(name, args)],
564 })
565}
566
567fn parse_function_args(s: &str) -> Option<Vec<Expression>> {
569 let s = s.trim();
570
571 if s.is_empty() {
572 return Some(Vec::new());
573 }
574
575 let mut args = Vec::new();
576 let mut current_start = 0;
577 let mut paren_depth: u32 = 0;
578 let bytes = s.as_bytes();
579
580 for (i, &b) in bytes.iter().enumerate() {
581 match b {
582 b'(' => paren_depth += 1,
583 b')' => paren_depth = paren_depth.saturating_sub(1),
584 b',' if paren_depth == 0 => {
585 let arg = &s[current_start..i];
586 let expr = parse_pipeline(arg.trim())?;
587 args.push(expr);
588 current_start = i + 1;
589 }
590 _ => {}
591 }
592 }
593
594 if current_start < s.len() {
596 let arg = &s[current_start..];
597 let expr = parse_pipeline(arg.trim())?;
598 args.push(expr);
599 }
600
601 Some(args)
602}
603
604fn parse_pipe_stage(s: &str) -> Option<PipeStage> {
606 let s = s.trim();
607
608 if let Some(paren_pos) = s.find('(') {
610 let name = &s[..paren_pos];
611 if !is_valid_identifier(name) {
612 return None;
613 }
614
615 if !s.ends_with(')') {
616 return None;
617 }
618
619 let args_str = &s[paren_pos + 1..s.len() - 1];
620 let args = parse_function_args(args_str)?;
621
622 return Some(PipeStage::with_args(name, args));
623 }
624
625 if is_valid_identifier(s) {
627 return Some(PipeStage::new(s));
628 }
629
630 None
631}
632
633fn is_valid_identifier(s: &str) -> bool {
636 let mut chars = s.chars();
637 match chars.next() {
638 None => false,
639 Some(first) => {
640 (first.is_ascii_alphabetic() || first == '_')
641 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
642 }
643 }
644}
645
646pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
650 let mut results = Vec::new();
651 let bytes = s.as_bytes();
652 let mut i = 0;
653
654 while i < bytes.len() {
655 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
656 let mut depth = 1;
658 let mut j = i + 2;
659 while j < bytes.len() && depth > 0 {
660 match bytes[j] {
661 b'{' => depth += 1,
662 b'}' => depth -= 1,
663 _ => {}
664 }
665 j += 1;
666 }
667
668 if depth == 0 {
669 let expr_str = &s[i..j];
670 if let Some(expression) = parse_expression(expr_str) {
671 results.push(LocatedExpression {
672 expression,
673 range: i..j,
674 });
675 }
676 i = j;
677 continue;
678 }
679 }
680 i += 1;
681 }
682
683 results
684}
685
686pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
710 parse_expr_value_at(json, 0)
711}
712
713const MAX_VALUE_DEPTH: usize = 64;
718
719fn parse_expr_value_at(json: &serde_json::Value, depth: usize) -> ExprValue {
728 if depth >= MAX_VALUE_DEPTH {
729 return ExprValue::Literal(Literal::Null);
730 }
731 let child_depth = depth.saturating_add(1);
732 match json {
733 serde_json::Value::String(s) => parse_string_to_expr_value(s),
734 serde_json::Value::Number(n) => {
735 if let Some(i) = n.as_i64() {
736 ExprValue::Literal(Literal::Integer(i))
737 } else if let Some(f) = n.as_f64() {
738 ExprValue::Literal(Literal::Float(f))
739 } else {
740 ExprValue::Literal(Literal::Integer(0))
742 }
743 }
744 serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
745 serde_json::Value::Null => ExprValue::Literal(Literal::Null),
746 serde_json::Value::Object(map) => {
747 let mut result = std::collections::HashMap::new();
748 for (k, v) in map {
749 result.insert(k.clone(), parse_expr_value_at(v, child_depth));
750 }
751 ExprValue::Object(result)
752 }
753 serde_json::Value::Array(arr) => {
754 let result: Vec<_> = arr
755 .iter()
756 .map(|v| parse_expr_value_at(v, child_depth))
757 .collect();
758 ExprValue::Array(result)
759 }
760 }
761}
762
763fn parse_string_to_expr_value(s: &str) -> ExprValue {
770 let trimmed = s.trim();
771
772 if trimmed.starts_with("${")
774 && trimmed.ends_with('}')
775 && !trimmed[2..trimmed.len() - 1].contains("${")
776 && let Some(expr) = parse_expression(trimmed)
777 {
778 return ExprValue::Expr(expr);
779 }
780
781 if s.contains("${") {
783 let parts = parse_interpolated_string(s);
784 if parts.len() == 1 {
785 if let StringPart::Literal(lit) = &parts[0] {
787 return ExprValue::Literal(Literal::String(lit.clone()));
788 }
789 }
790 return ExprValue::Interpolated(parts);
791 }
792
793 ExprValue::Literal(Literal::String(s.to_string()))
795}
796
797fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
801 let mut parts = Vec::new();
802 let bytes = s.as_bytes();
803 let mut last_end = 0;
804 let mut i = 0;
805
806 while i < bytes.len() {
807 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
808 if i > last_end {
810 parts.push(StringPart::Literal(s[last_end..i].to_string()));
811 }
812
813 let mut depth = 1;
815 let mut j = i + 2;
816 while j < bytes.len() && depth > 0 {
817 match bytes[j] {
818 b'{' => depth += 1,
819 b'}' => depth -= 1,
820 _ => {}
821 }
822 j += 1;
823 }
824
825 if depth == 0 {
826 let expr_str = &s[i..j];
827 if let Some(expr) = parse_expression(expr_str) {
828 parts.push(StringPart::Expr(expr));
829 } else {
830 parts.push(StringPart::Literal(expr_str.to_string()));
832 }
833 last_end = j;
834 i = j;
835 continue;
836 }
837 }
838 i += 1;
839 }
840
841 if last_end < s.len() {
843 parts.push(StringPart::Literal(s[last_end..].to_string()));
844 }
845
846 parts
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn parse_simple_reference() {
855 let expr = parse_expression("${artifact.ksr}").unwrap();
856 match expr {
857 Expression::Reference(r) => {
858 assert_eq!(r.ref_type, RefType::Artifact);
859 assert_eq!(r.name, "ksr");
860 assert!(r.property.is_none());
861 }
862 _ => panic!("Expected Reference"),
863 }
864 }
865
866 #[test]
867 fn parse_reference_with_property() {
868 let expr = parse_expression("${artifact.keypair.private}").unwrap();
869 match expr {
870 Expression::Reference(r) => {
871 assert_eq!(r.ref_type, RefType::Artifact);
872 assert_eq!(r.name, "keypair");
873 assert_eq!(r.property, Some("private".to_string()));
874 }
875 _ => panic!("Expected Reference"),
876 }
877 }
878
879 #[test]
880 fn parse_param_reference() {
881 let expr = parse_expression("${param.key_label}").unwrap();
882 match expr {
883 Expression::Reference(r) => {
884 assert_eq!(r.ref_type, RefType::Param);
885 assert_eq!(r.name, "key_label");
886 }
887 _ => panic!("Expected Reference"),
888 }
889 }
890
891 #[test]
892 fn parse_simple_pipeline() {
893 let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
894 match expr {
895 Expression::Pipeline { source, stages } => {
896 assert!(matches!(*source, Expression::Reference(_)));
897 assert_eq!(stages.len(), 1);
898 assert_eq!(stages[0].function, "sha256");
899 assert!(stages[0].args.is_empty());
900 }
901 _ => panic!("Expected Pipeline"),
902 }
903 }
904
905 #[test]
906 fn parse_multi_stage_pipeline() {
907 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
908 match expr {
909 Expression::Pipeline { stages, .. } => {
910 assert_eq!(stages.len(), 2);
911 assert_eq!(stages[0].function, "sha256");
912 assert_eq!(stages[1].function, "hex");
913 }
914 _ => panic!("Expected Pipeline"),
915 }
916 }
917
918 #[test]
919 fn parse_pipeline_with_function_args() {
920 let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
921 match expr {
922 Expression::Pipeline { stages, .. } => {
923 assert_eq!(stages.len(), 1);
924 assert_eq!(stages[0].function, "substr");
925 assert_eq!(stages[0].args.len(), 2);
926 }
927 _ => panic!("Expected Pipeline"),
928 }
929 }
930
931 #[test]
932 fn parse_complex_pipeline() {
933 let expr =
934 parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
935 match expr {
936 Expression::Pipeline { source, stages } => {
937 if let Expression::Reference(r) = *source {
938 assert_eq!(r.name, "keypair");
939 assert_eq!(r.property, Some("public".to_string()));
940 }
941 assert_eq!(stages.len(), 3);
942 assert_eq!(stages[0].function, "fingerprint");
943 assert_eq!(stages[1].function, "hex");
944 assert_eq!(stages[2].function, "upper");
945 }
946 _ => panic!("Expected Pipeline"),
947 }
948 }
949
950 #[test]
951 fn parse_invalid_expression() {
952 assert!(parse_expression("not an expression").is_none());
953 assert!(parse_expression("${").is_none());
954 assert!(parse_expression("${}").is_none());
955 assert!(parse_expression("${invalid}").is_none()); }
957
958 #[test]
959 fn find_expressions_in_string() {
960 let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
961 assert_eq!(exprs.len(), 1);
962 assert_eq!(exprs[0].range.start, 6);
963 }
964
965 #[test]
966 fn find_multiple_expressions() {
967 let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
968 assert_eq!(exprs.len(), 2);
969 }
970
971 #[test]
972 fn value_type_names() {
973 assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
974 assert_eq!(Value::String(String::new()).type_name(), "string");
975 assert_eq!(Value::Integer(0).type_name(), "integer");
976 assert_eq!(Value::Boolean(true).type_name(), "boolean");
977 assert_eq!(Value::Null.type_name(), "null");
978 }
979
980 #[test]
981 fn value_as_bytes() {
982 let v = Value::Bytes(vec![1, 2, 3]);
983 assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
984
985 let v = Value::String("hello".into());
986 assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
987
988 let v = Value::Integer(42);
989 assert!(v.as_bytes().is_none());
990 }
991
992 mod ceremony_examples {
993 use super::*;
994
995 #[test]
997 fn dnssec_hash_verification() {
998 let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
1000 match expr {
1001 Expression::Pipeline { source, stages } => {
1002 if let Expression::Reference(r) = *source {
1003 assert_eq!(r.ref_type, RefType::Artifact);
1004 assert_eq!(r.name, "ksr");
1005 } else {
1006 panic!("Expected Reference source");
1007 }
1008 assert_eq!(stages.len(), 3);
1010 assert_eq!(stages[0].function, "sha256");
1011 assert_eq!(stages[1].function, "hex");
1012 assert_eq!(stages[2].function, "substr");
1013 assert_eq!(stages[2].args.len(), 2);
1014 }
1015 _ => panic!("Expected Pipeline"),
1016 }
1017 }
1018
1019 #[test]
1020 fn dnssec_param_reference() {
1021 let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
1023 match expr {
1024 Expression::Reference(r) => {
1025 assert_eq!(r.ref_type, RefType::Param);
1026 assert_eq!(r.name, "expected_ksr_hash");
1027 }
1028 _ => panic!("Expected Reference"),
1029 }
1030 }
1031
1032 #[test]
1033 fn dnssec_oral_readback() {
1034 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1036 match expr {
1037 Expression::Pipeline { stages, .. } => {
1038 assert_eq!(stages.len(), 2);
1039 assert_eq!(stages[0].function, "sha256");
1040 assert_eq!(stages[1].function, "hex");
1041 }
1042 _ => panic!("Expected Pipeline"),
1043 }
1044 }
1045
1046 #[test]
1048 fn hsm_artifact_reference() {
1049 let expr = parse_expression("${artifact.master_keypair}").unwrap();
1051 match expr {
1052 Expression::Reference(r) => {
1053 assert_eq!(r.ref_type, RefType::Artifact);
1054 assert_eq!(r.name, "master_keypair");
1055 assert!(r.property.is_none());
1056 }
1057 _ => panic!("Expected Reference"),
1058 }
1059 }
1060
1061 #[test]
1062 fn hsm_public_key_fingerprint() {
1063 let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1065 match expr {
1066 Expression::Pipeline { source, stages } => {
1067 if let Expression::Reference(r) = *source {
1068 assert_eq!(r.name, "master_public_key");
1069 }
1070 assert_eq!(stages.len(), 2);
1071 }
1072 _ => panic!("Expected Pipeline"),
1073 }
1074 }
1075
1076 #[test]
1078 fn crypto_artifact_with_property() {
1079 let expr = parse_expression("${artifact.keypair.private}").unwrap();
1081 match expr {
1082 Expression::Reference(r) => {
1083 assert_eq!(r.ref_type, RefType::Artifact);
1084 assert_eq!(r.name, "keypair");
1085 assert_eq!(r.property, Some("private".to_string()));
1086 }
1087 _ => panic!("Expected Reference"),
1088 }
1089 }
1090
1091 #[test]
1093 fn find_embedded_expressions() {
1094 let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1096 assert_eq!(exprs.len(), 1);
1097 if let Expression::Reference(r) = &exprs[0].expression {
1098 assert_eq!(r.ref_type, RefType::Param);
1099 assert_eq!(r.name, "hsm_serial");
1100 }
1101 }
1102
1103 #[test]
1104 fn find_multiple_embedded_params() {
1105 let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1107 assert_eq!(exprs.len(), 2);
1108 }
1109
1110 #[test]
1112 fn role_reference_parses() {
1113 let expr = parse_expression("${role.ceremony_admin}").unwrap();
1114 match expr {
1115 Expression::Reference(r) => {
1116 assert_eq!(r.ref_type, RefType::Role);
1117 assert_eq!(r.name, "ceremony_admin");
1118 }
1119 _ => panic!("Expected Reference"),
1120 }
1121 }
1122 }
1123
1124 mod expr_value_tests {
1125 use super::*;
1126
1127 #[test]
1128 fn parse_literal_string() {
1129 let json = serde_json::json!("hello");
1130 let expr = parse_expr_value(&json);
1131 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1132 }
1133
1134 #[test]
1135 fn parse_literal_integer() {
1136 let json = serde_json::json!(42);
1137 let expr = parse_expr_value(&json);
1138 assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1139 }
1140
1141 #[test]
1142 fn parse_literal_float() {
1143 let json = serde_json::json!(1.5);
1144 let expr = parse_expr_value(&json);
1145 assert!(
1146 matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1147 );
1148 }
1149
1150 #[test]
1151 fn parse_literal_boolean() {
1152 let json = serde_json::json!(true);
1153 let expr = parse_expr_value(&json);
1154 assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1155 }
1156
1157 #[test]
1158 fn parse_literal_null() {
1159 let json = serde_json::Value::Null;
1160 let expr = parse_expr_value(&json);
1161 assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1162 }
1163
1164 #[test]
1165 fn parse_single_expression() {
1166 let json = serde_json::json!("${artifact.ksr | sha256}");
1167 let expr = parse_expr_value(&json);
1168 match expr {
1169 ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1170 assert!(matches!(*source, Expression::Reference(_)));
1171 assert_eq!(stages.len(), 1);
1172 assert_eq!(stages[0].function, "sha256");
1173 }
1174 _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1175 }
1176 }
1177
1178 #[test]
1179 fn parse_simple_reference() {
1180 let json = serde_json::json!("${param.name}");
1181 let expr = parse_expr_value(&json);
1182 match expr {
1183 ExprValue::Expr(Expression::Reference(r)) => {
1184 assert_eq!(r.ref_type, RefType::Param);
1185 assert_eq!(r.name, "name");
1186 }
1187 _ => panic!("Expected Expr(Reference), got {expr:?}"),
1188 }
1189 }
1190
1191 #[test]
1192 fn parse_interpolated_string() {
1193 let json = serde_json::json!("Hello, ${param.name}!");
1194 let expr = parse_expr_value(&json);
1195 match expr {
1196 ExprValue::Interpolated(parts) => {
1197 assert_eq!(parts.len(), 3);
1198 assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1199 assert!(matches!(
1200 &parts[1],
1201 StringPart::Expr(Expression::Reference(_))
1202 ));
1203 assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1204 }
1205 _ => panic!("Expected Interpolated, got {expr:?}"),
1206 }
1207 }
1208
1209 #[test]
1210 fn parse_object_with_expressions() {
1211 let json = serde_json::json!({
1212 "actual": "${artifact.ksr | sha256 | hex}",
1213 "expected": "${param.expected_hash}",
1214 "static": "hello"
1215 });
1216 let expr = parse_expr_value(&json);
1217 match expr {
1218 ExprValue::Object(map) => {
1219 assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1220 assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1221 assert!(matches!(
1222 map.get("static"),
1223 Some(ExprValue::Literal(Literal::String(_)))
1224 ));
1225 }
1226 _ => panic!("Expected Object, got {expr:?}"),
1227 }
1228 }
1229
1230 #[test]
1231 fn parse_array_with_expressions() {
1232 let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1233 let expr = parse_expr_value(&json);
1234 match expr {
1235 ExprValue::Array(arr) => {
1236 assert_eq!(arr.len(), 3);
1237 assert!(matches!(&arr[0], ExprValue::Expr(_)));
1238 assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1239 assert!(matches!(&arr[2], ExprValue::Expr(_)));
1240 }
1241 _ => panic!("Expected Array, got {expr:?}"),
1242 }
1243 }
1244
1245 #[test]
1246 fn parse_deeply_nested_value_is_truncated_not_crashed() {
1247 let mut json = serde_json::json!("leaf");
1251 for _ in 0..200 {
1252 json = serde_json::Value::Array(vec![json]);
1253 }
1254 let mut expr = parse_expr_value(&json);
1255 let mut levels = 0;
1256 loop {
1257 match expr {
1258 ExprValue::Array(mut arr) => {
1259 assert_eq!(arr.len(), 1);
1260 expr = arr.remove(0);
1261 levels += 1;
1262 }
1263 other => {
1264 assert!(
1265 matches!(other, ExprValue::Literal(Literal::Null)),
1266 "truncated tail must be Literal(Null), got {other:?}"
1267 );
1268 break;
1269 }
1270 }
1271 }
1272 assert_eq!(levels, MAX_VALUE_DEPTH, "recursion must stop at the cap");
1273 }
1274
1275 #[test]
1276 fn parse_moderately_nested_value_is_preserved() {
1277 let mut json = serde_json::json!("leaf");
1278 for _ in 0..10 {
1279 json = serde_json::Value::Array(vec![json]);
1280 }
1281 let mut expr = parse_expr_value(&json);
1282 for _ in 0..10 {
1283 match expr {
1284 ExprValue::Array(mut arr) => {
1285 assert_eq!(arr.len(), 1);
1286 expr = arr.remove(0);
1287 }
1288 other => panic!("expected Array, got {other:?}"),
1289 }
1290 }
1291 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "leaf"));
1292 }
1293
1294 #[test]
1295 fn parse_multiple_expressions_in_string() {
1296 let json = serde_json::json!("${param.a} and ${param.b}");
1297 let expr = parse_expr_value(&json);
1298 match expr {
1299 ExprValue::Interpolated(parts) => {
1300 assert_eq!(parts.len(), 3);
1301 assert!(matches!(&parts[0], StringPart::Expr(_)));
1302 assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1303 assert!(matches!(&parts[2], StringPart::Expr(_)));
1304 }
1305 _ => panic!("Expected Interpolated, got {expr:?}"),
1306 }
1307 }
1308 }
1309}