diff --git a/src/node/mod.rs b/src/node/mod.rs index 4965f74..e1b417a 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use yaml_merge_keys::merge_keys_serde; use crate::list::{List, RemovableList, UniqueList}; -use crate::refs::Token; +use crate::refs::{ResolveState, Token}; use crate::types::{Mapping, Value}; use crate::{to_lexical_absolute, Reclass}; @@ -216,7 +216,10 @@ impl Node { if let Some(clstoken) = clstoken { // If we got a token, render it, and convert it into a string with // `raw_string()` to ensure no spurious quotes are injected. - clstoken.render(&root.parameters)?.raw_string()? + let mut state = ResolveState::default(); + clstoken + .render(&root.parameters, &mut state)? + .raw_string()? } else { // If Token::parse() returns None, the class name can't contain any references, // just convert cls into an owned String. diff --git a/src/refs/mod.rs b/src/refs/mod.rs index bfb3eb8..2dd4a40 100644 --- a/src/refs/mod.rs +++ b/src/refs/mod.rs @@ -3,6 +3,7 @@ mod parser; use crate::types::{Mapping, Value}; use anyhow::{anyhow, Result}; use nom::error::{convert_error, VerboseError}; +use std::collections::HashSet; #[derive(Debug, PartialEq, Eq)] /// Represents a parsed Reclass reference @@ -16,6 +17,33 @@ pub enum Token { Combined(Vec), } +#[derive(Clone, Debug, Default)] +pub struct ResolveState { + /// Reference paths which we've seen during reference resolution + seen_paths: HashSet, + /// Recursion depth of the resolution (in number of calls to Token::resolve() for Token::Ref + /// objects). + depth: usize, +} + +impl ResolveState { + /// Formats paths that have been seen as a comma-separated list. + fn seen_paths_list(&self) -> String { + let mut paths = self + .seen_paths + .iter() + .map(|p| format!("\"{p}\"")) + .collect::>(); + paths.sort(); + paths.join(", ") + } +} + +/// Maximum allowed recursion depth for Token::resolve(). We're fairly conservative with the value, +/// since it's rather unlikely that a well-formed inventory will have any references that are +/// nested deeper than 64. +const RESOLVE_MAX_DEPTH: usize = 64; + impl Token { /// Parses an arbitrary string into a `Token`. Returns None, if the string doesn't contain any /// opening reference markers. @@ -48,25 +76,25 @@ impl Token { /// the Mapping provided through parameter `params`. /// /// The heavy lifting is done by `Token::resolve()`. - pub fn render(&self, params: &Mapping) -> Result { + pub fn render(&self, params: &Mapping, state: &mut ResolveState) -> Result { if self.is_ref() { // handle value refs (i.e. refs where the full value of the key is replaced) // We call `interpolate()` after `resolve()` to ensure that we fully interpolate all // references if the result of `resolve()` is a complex Value (Mapping or Sequence). - self.resolve(params)?.interpolate(params) + self.resolve(params, state)?.interpolate(params, state) } else { - Ok(Value::Literal(self.resolve(params)?.raw_string()?)) + Ok(Value::Literal(self.resolve(params, state)?.raw_string()?)) } } /// Resolves the Token into a [`Value`]. References are looked up in the provided `params` /// Mapping. - fn resolve(&self, params: &Mapping) -> Result { + fn resolve(&self, params: &Mapping, state: &mut ResolveState) -> Result { match self { // Literal tokens can be directly turned into `Value::Literal` Self::Literal(s) => Ok(Value::Literal(s.to_string())), Self::Combined(tokens) => { - let res = interpolate_token_slice(tokens, params)?; + let res = interpolate_token_slice(tokens, params, state)?; // The result of `interpolate_token_slice()` for a `Token::Combined()` can't result // in more unresolved refs since we iterate over each segment until there's no // Value::String() left, so we return a Value::Literal(). @@ -76,9 +104,33 @@ impl Token { // `interpolate_token_slice()`. Then we split the resolved reference path into segments // on `:` and iteratively look up each segment in the provided `params` Mapping. Self::Ref(parts) => { + // We track the number of calls to `Token::resolve()` for Token::Ref that the + // current `state` has seen in state.depth. + state.depth += 1; + if state.depth > RESOLVE_MAX_DEPTH { + // If we've called `Token::resolve()` more than RESOLVE_MAX_DEPTH (64) times + // recursively, it's likely that there's still an edge case where we don't + // detect a reference loop with the current reference path tracking + // implementation. We abort at a recursion depth of 64, since it's quite + // unlikely that there's a legitimate case where we have a recursion depth of + // 64 when resolving references for a well formed inventory. + let paths = state.seen_paths_list(); + return Err(anyhow!( + "Token resolution exceeded recursion depth of {RESOLVE_MAX_DEPTH}. \ + We've seen the following reference paths: [{paths}].", + )); + } // Construct flattened ref path by resolving any potential nested references in the // Ref's Vec. - let path = interpolate_token_slice(parts, params)?; + let path = interpolate_token_slice(parts, params, state)?; + + if state.seen_paths.contains(&path) { + // we've already seen this reference, so we know there's a loop, and can abort + // resolution. + let paths = state.seen_paths_list(); + return Err(anyhow!("Reference loop with reference paths [{paths}].")); + } + state.seen_paths.insert(path.clone()); // generate iterator containing flattened reference path segments let mut refpath_iter = path.split(':'); @@ -122,7 +174,7 @@ impl Token { // individual references are resolved, and always do value lookups on // resolved references. Value::String(_) => { - newv = v.interpolate(params)?; + newv = v.interpolate(params, state)?; v = newv.get(&key.into()).ok_or_else(|| { anyhow!("unable to lookup key '{key}' for '{path}'") })?; @@ -130,8 +182,12 @@ impl Token { Value::ValueList(l) => { let mut i = vec![]; for v in l { + // When resolving references in ValueLists, we want to track state + // separately for each layer, since reference loops can't be + // stretched across layers. + let mut st = state.clone(); let v = if v.is_string() { - v.interpolate(params)? + v.interpolate(params, &mut st)? } else { v.clone() }; @@ -159,9 +215,9 @@ impl Token { let mut v = v.clone(); // Finally, we iteratively interpolate `v` while it's a `Value::String()` or // `Value::ValueList`. This ensures that the returned Value will never contain - // further references. + // further references. Here, we want to continue tracking the state normally. while v.is_string() || v.is_value_list() { - v = v.interpolate(params)?; + v = v.interpolate(params, state)?; } Ok(v) } @@ -195,15 +251,23 @@ impl std::fmt::Display for Token { /// Interpolate a `Vec`. Called from `Token::resolve()` for `Token::Combined` and /// `Token::Ref` Vecs. -fn interpolate_token_slice(tokens: &[Token], params: &Mapping) -> Result { +fn interpolate_token_slice( + tokens: &[Token], + params: &Mapping, + state: &mut ResolveState, +) -> Result { // Iterate through each element of the Vec, and call Token::resolve() on each element. // Additionally, we repeatedly call `Value::interpolate()` on the resolved value for each // element, as long as that Value is a `Value::String`. let mut res = String::new(); for t in tokens { - let mut v = t.resolve(params)?; + // Multiple separate refs in a combined or ref token can't form loops between each other. + // Each individual ref can still be part of a loop, so we make a fresh copy of the input + // state before resolving each element. + let mut st = state.clone(); + let mut v = t.resolve(params, &mut st)?; while v.is_string() { - v = v.interpolate(params)?; + v = v.interpolate(params, &mut st)?; } res.push_str(&v.raw_string()?); } diff --git a/src/refs/token_resolve_parse_tests.rs b/src/refs/token_resolve_parse_tests.rs index b47c553..c6d7c15 100644 --- a/src/refs/token_resolve_parse_tests.rs +++ b/src/refs/token_resolve_parse_tests.rs @@ -6,7 +6,8 @@ fn test_resolve_ref_str() { let token = Token::Ref(vec![Token::literal_from_str("foo")]); let params = Mapping::from_str("foo: bar").unwrap(); - let v = token.resolve(¶ms).unwrap(); + let mut state = ResolveState::default(); + let v = token.resolve(¶ms, &mut state).unwrap(); assert_eq!(v, Value::Literal("bar".into())); } @@ -15,7 +16,8 @@ fn test_resolve_ref_val() { let token = Token::Ref(vec![Token::literal_from_str("foo")]); let params = Mapping::from_str("foo: True").unwrap(); - let v = token.resolve(¶ms).unwrap(); + let mut state = ResolveState::default(); + let v = token.resolve(¶ms, &mut state).unwrap(); assert_eq!(v, Value::Bool(true)); } @@ -24,7 +26,8 @@ fn test_resolve_literal() { let token = Token::literal_from_str("foo"); let params = Mapping::new(); - let v = token.resolve(¶ms).unwrap(); + let mut state = ResolveState::default(); + let v = token.resolve(¶ms, &mut state).unwrap(); assert_eq!(v, Value::Literal("foo".into())); } @@ -36,7 +39,8 @@ fn test_resolve_combined() { ]); let params = Mapping::from_str("{foo: bar, bar: baz}").unwrap(); - let v = token.resolve(¶ms).unwrap(); + let mut state = ResolveState::default(); + let v = token.resolve(¶ms, &mut state).unwrap(); assert_eq!(v, Value::Literal("foobar".into())); } #[test] @@ -48,7 +52,8 @@ fn test_resolve_combined_2() { ]); let params = Mapping::from_str(r#"{foo: "${bar}", bar: baz}"#).unwrap(); - let v = token.resolve(¶ms).unwrap(); + let mut state = ResolveState::default(); + let v = token.resolve(¶ms, &mut state).unwrap(); assert_eq!(v, Value::Literal("foobaz".into())); } @@ -64,7 +69,8 @@ fn test_resolve_combined_3() { "#; let params = Mapping::from_str(params).unwrap(); - let v = token.resolve(¶ms).unwrap(); + let mut state = ResolveState::default(); + let v = token.resolve(¶ms, &mut state).unwrap(); assert_eq!(v, Value::Literal("foo${bar}".into())); } @@ -105,7 +111,11 @@ fn test_resolve() { let p = Mapping::from_str("foo: foo").unwrap(); let reftoken = parse_ref(&"${foo}").unwrap(); - assert_eq!(reftoken.resolve(&p).unwrap(), Value::Literal("foo".into())); + let mut state = ResolveState::default(); + assert_eq!( + reftoken.resolve(&p, &mut state).unwrap(), + Value::Literal("foo".into()) + ); } #[test] @@ -113,7 +123,9 @@ fn test_resolve_subkey() { let p = Mapping::from_str("foo: {foo: foo}").unwrap(); let reftoken = parse_ref(&"${foo:foo}").unwrap(); - assert_eq!(reftoken.resolve(&p).unwrap(), Value::Literal("foo".into())); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); + assert_eq!(v, Value::Literal("foo".into())); } #[test] @@ -121,7 +133,9 @@ fn test_resolve_nested() { let p = Mapping::from_str("{foo: foo, bar: {foo: foo}}").unwrap(); let reftoken = parse_ref(&"${bar:${foo}}").unwrap(); - assert_eq!(reftoken.resolve(&p).unwrap(), Value::Literal("foo".into())); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); + assert_eq!(v, Value::Literal("foo".into())); } #[test] @@ -135,10 +149,9 @@ fn test_resolve_nested_subkey() { // ${bar:${foo:bar}} == ${bar:foo} == foo let reftoken = parse_ref(&"${bar:${foo:bar}}").unwrap(); - assert_eq!( - reftoken.resolve(&p).unwrap(), - Value::Literal("foo".to_string()) - ); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); + assert_eq!(v, Value::Literal("foo".to_string())); } #[test] @@ -152,10 +165,9 @@ fn test_resolve_kapitan_secret_ref() { let reftoken = parse_ref(&"?{vaultkv:foo/bar/${baz:baz}/qux}").unwrap(); dbg!(&reftoken); - assert_eq!( - reftoken.resolve(&p).unwrap(), - Value::Literal("?{vaultkv:foo/bar/baz/qux}".to_string()) - ); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); + assert_eq!(v, Value::Literal("?{vaultkv:foo/bar/baz/qux}".to_string())); } #[test] @@ -168,10 +180,9 @@ fn test_resolve_escaped_ref() { let p = Mapping::from_str(params).unwrap(); let reftoken = parse_ref("\\${PROJECT_LABEL}").unwrap(); - assert_eq!( - reftoken.resolve(&p).unwrap(), - Value::Literal("${PROJECT_LABEL}".to_string()) - ); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); + assert_eq!(v, Value::Literal("${PROJECT_LABEL}".to_string())); } #[test] @@ -183,8 +194,10 @@ fn test_resolve_mapping_value() { "#; let p = Mapping::from_str(p).unwrap(); let reftoken = parse_ref("${foo}").unwrap(); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); assert_eq!( - reftoken.resolve(&p).unwrap(), + v, Value::Mapping(Mapping::from_str("{bar: bar, baz: baz}").unwrap()) ); } @@ -198,10 +211,54 @@ fn test_resolve_mapping_embedded() { "#; let p = Mapping::from_str(p).unwrap(); let reftoken = parse_ref("foo: ${foo}").unwrap(); + let mut state = ResolveState::default(); + let v = reftoken.resolve(&p, &mut state).unwrap(); assert_eq!( - reftoken.resolve(&p).unwrap(), + v, // Mapping is serialized as JSON when embedded in a string. serde_json emits JSON maps // with lexically sorted keys and minimal whitespace. Value::Literal(r#"foo: {"bar":"bar","baz":"baz"}"#.to_string()) ); } + +#[test] +#[should_panic(expected = "Reference loop with reference paths [\"foo\"].")] +fn test_resolve_recursive_error() { + let p = r#" + foo: ${foo} + "#; + let p = Mapping::from_str(p).unwrap(); + let reftoken = parse_ref("${foo}").unwrap(); + + let mut state = ResolveState::default(); + let _v = reftoken.resolve(&p, &mut state).unwrap(); +} + +#[test] +#[should_panic(expected = "Reference loop with reference paths [\"bar\", \"foo\"].")] +fn test_resolve_recursive_error_2() { + let p = r#" + foo: ${bar} + bar: ${foo} + "#; + let p = Mapping::from_str(p).unwrap(); + let reftoken = parse_ref("${foo}").unwrap(); + + let mut state = ResolveState::default(); + let _v = reftoken.resolve(&p, &mut state).unwrap(); +} + +#[test] +#[should_panic(expected = "Reference loop with reference paths [\"baz\", \"foo\"].")] +fn test_resolve_nested_recursive_error() { + let p = r#" + foo: ${baz} + baz: + qux: ${foo} + "#; + let p = Mapping::from_str(p).unwrap(); + let reftoken = parse_ref("${foo}").unwrap(); + + let mut state = ResolveState::default(); + let _v = reftoken.resolve(&p, &mut state).unwrap(); +} diff --git a/src/types/mapping.rs b/src/types/mapping.rs index f9f6b2d..85d960f 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -10,6 +10,7 @@ use std::hash::{Hash, Hasher}; use super::value::Value; use super::KeyPrefix; +use crate::refs::ResolveState; /// Represents a YAML mapping in a form suitable to manage Reclass parameters. /// @@ -356,10 +357,16 @@ impl Mapping { /// The method looks up reference values in parameter `root`. After interpolation of each /// Mapping key-value pair, the resulting value is flattened before it's inserted in the new /// Mapping. Mapping keys are inserted into the new mapping unchanged. - pub(super) fn interpolate(&self, root: &Self) -> Result { + pub(super) fn interpolate(&self, root: &Self, state: &mut ResolveState) -> Result { let mut res = Self::new(); for (k, v) in self { - let mut v = v.interpolate(root)?; + // Reference loops in mappings can't be stretched across key-value pairs, so we pass a + // copy of the resolution state we're called with to the `interpolate` call for each + // value. Also, we don't need to update the state which we were called with, since we + // either manage to interpolate a value (in which case it doesn't contain a loop) or we + // don't and the whole interpolation is aborted. + let mut st = state.clone(); + let mut v = v.interpolate(root, &mut st)?; v.flatten()?; res.insert(k.clone(), v)?; } diff --git a/src/types/value.rs b/src/types/value.rs index 82e5fb8..a405275 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -8,7 +8,7 @@ use std::mem; use super::KeyPrefix; use super::{Mapping, Sequence}; -use crate::refs::Token; +use crate::refs::{ResolveState, Token}; /// Represents a YAML value in a form suitable for processing Reclass parameters. #[derive(Clone, Debug, PartialEq)] @@ -528,14 +528,14 @@ impl Value { /// /// Note that users should prefer calling `Value::rendered()` or one of its in-place variants /// over this method. - pub(crate) fn interpolate(&self, root: &Mapping) -> Result { + pub(crate) fn interpolate(&self, root: &Mapping, state: &mut ResolveState) -> Result { Ok(match self { Self::String(s) => { // String interpolation parses any Reclass references in the String and resolves // them. The result of `Token::render()` can be an arbitrary Value, except for // `Value::String()`, since `render()` will recursively call `interpolate()`. if let Some(token) = Token::parse(s)? { - token.render(root)? + token.render(root, state)? } else { // If Token::parse() returns None, we can be sure that there's no references // int the String, and just return the string as a `Value::Literal`. @@ -543,12 +543,18 @@ impl Value { } } // Mappings are interpolated by calling `Mapping::interpolate()`. - Self::Mapping(m) => Self::Mapping(m.interpolate(root)?), + Self::Mapping(m) => Self::Mapping(m.interpolate(root, state)?), Self::Sequence(s) => { // Sequences are interpolated by calling interpolate() for each element. let mut seq = vec![]; for it in s { - let e = it.interpolate(root)?; + // References in separate entries in sequences can't form loops. Therefore we + // pass a copy of the current resolution state to the recursive call for each + // element. We don't need to update the input state after we're done with a + // Sequence either, since there's no potential to start recursing again, if + // we've fully interpolated a Sequence. + let mut st = state.clone(); + let e = it.interpolate(root, &mut st)?; seq.push(e); } Self::Sequence(seq) @@ -561,14 +567,23 @@ impl Value { // NOTE(sg): Empty ValueLists are interpolated as Value::Null. let mut r = Value::Null; for v in l { - r.merge(v.interpolate(root)?)?; + // For each ValueList layer, we pass a copy of the current resolution state to + // the recursive call to interpolate, since references in different ValueList + // layers can't form loops with each other (Intuitively: either we manage to + // resolve all references in a ValueList layer, or we don't, but once we're + // done with a layer, any references that we saw there have been successfully + // resolved, and don't matter for the next layer we're interpolating). + let mut st = state.clone(); + r.merge(v.interpolate(root, &mut st)?)?; } // Depending on the structure of the ValueList, we may end up with a final // interpolated Value which contains more ValueLists due to mapping merges. Such // ValueLists can themselves contain further references. To handle this case, we // call `interpolate()` again to resolve those references. This recursion stops // once `Token::render()` doesn't produce new `Value::String()`. - r.interpolate(root)? + // For this interpolation, we need to actually update the resolution state, so we + // pass in the `state` which we were called with. + r.interpolate(root, state)? } _ => self.clone(), }) @@ -704,7 +719,10 @@ impl Value { /// reference keys in `root`. After all references have been interpolated, the method flattens /// any remaining ValueLists and returns the final "flattened" value. pub fn rendered(&self, root: &Mapping) -> Result { - let mut v = self.interpolate(root)?; + let mut state = ResolveState::default(); + let mut v = self + .interpolate(root, &mut state) + .map_err(|e| anyhow!("While resolving references in {self}: {e}"))?; v.flatten()?; Ok(v) } diff --git a/src/types/value/value_flattened_tests.rs b/src/types/value/value_flattened_tests.rs index bf23d80..da62b0f 100644 --- a/src/types/value/value_flattened_tests.rs +++ b/src/types/value/value_flattened_tests.rs @@ -181,27 +181,27 @@ fn test_flattened_sequence_over_simple_value_error() { #[test] fn test_flattened_nested_mapping_value_list() { - // preprocess the valuelist entries by calling interpolate() on each entry to ensure we've + // preprocess the valuelist entries by calling render() on each entry to ensure we've // transformed all `Value::String()` to `Value::Literal()`. let v = Value::ValueList(vec![ Mapping::from_str("foo: {foo: {foo: foo}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("foo: {foo: {foo: bar}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("foo: {foo: {bar: bar}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("foo: {bar: {bar: bar}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), ]); @@ -217,32 +217,32 @@ fn test_flattened_nested_mapping_value_list() { #[test] fn test_flattened_nested_mapping_value_list_2() { - // preprocess the valuelist entries by calling interpolate() on each entry to ensure we've + // preprocess the valuelist entries by calling render() on each entry to ensure we've // transformed all `Value::String()` to `Value::Literal()`. let v = Value::ValueList(vec![ Mapping::from_str("qux: {foo: {foo: {foo: foo}}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("qux: {foo: {foo: {foo: bar}}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("qux: {foo: {foo: {bar: bar}}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("qux: {foo: {bar: {bar: bar}}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), Mapping::from_str("qux: {bar: {bar: {bar: bar}}}") .unwrap() - .interpolate(&Mapping::new()) + .render(&Mapping::new()) .unwrap() .into(), ]); diff --git a/src/types/value/value_interpolate_tests.rs b/src/types/value/value_interpolate_tests.rs index 7dd3bef..19bbc4c 100644 --- a/src/types/value/value_interpolate_tests.rs +++ b/src/types/value/value_interpolate_tests.rs @@ -2,12 +2,22 @@ use super::*; use std::str::FromStr; +impl Mapping { + pub(super) fn render(&self, root: &Self) -> Result { + let mut state = ResolveState::default(); + self.interpolate(root, &mut state) + } +} + fn sequence_literal(v: Vec) -> Value { - Value::Sequence(v).interpolate(&Mapping::new()).unwrap() + let mut state = ResolveState::default(); + Value::Sequence(v) + .interpolate(&Mapping::new(), &mut state) + .unwrap() } fn mapping_literal(m: Mapping) -> Value { - Value::Mapping(m.interpolate(&Mapping::new()).unwrap()) + Value::Mapping(m.render(&Mapping::new()).unwrap()) } #[test] @@ -23,7 +33,7 @@ fn test_extend_sequence() { .unwrap(); p.merge(&o).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!( p.get(&"l".into()).unwrap(), @@ -47,7 +57,7 @@ fn test_override_sequence() { .unwrap(); p.merge(&o).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!( p.get(&"l".into()).unwrap(), @@ -78,7 +88,7 @@ fn test_extend_mapping() { .unwrap(); p.merge(&o).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!(p.get(&"m".into()).unwrap(), &Value::Mapping(r)); } @@ -100,7 +110,7 @@ fn test_override_mapping() { .unwrap(); p.merge(&o).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!(p.get(&"m".into()).unwrap(), &Value::Mapping(n)); } @@ -153,7 +163,7 @@ fn test_embedded_ref() { .unwrap(); p.merge(&m).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!(p.get(&"foo".into()).unwrap(), &Value::Literal("foo".into())); assert_eq!( @@ -189,7 +199,7 @@ fn test_ref_in_sequence() { .unwrap(); p.merge(&m).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!(p.get(&"foo".into()).unwrap(), &Value::Literal("foo".into())); assert_eq!(p.get(&"bar".into()).unwrap(), &Value::Literal("bar".into())); @@ -220,7 +230,7 @@ fn test_nested_ref() { .unwrap(); p.merge(&m).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); assert_eq!( p.get(&"ref".into()).unwrap(), @@ -246,7 +256,7 @@ fn test_merge_over_ref() { let overlay = Mapping::from_str(overlay).unwrap(); p.merge(&overlay).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); dbg!(&p); let merged_foo = r#" @@ -279,7 +289,7 @@ fn test_merge_over_ref_nested() { let overlay = Mapping::from_str(overlay).unwrap(); p.merge(&overlay).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); let merged_some = r#" foo: @@ -314,7 +324,7 @@ fn test_merge_over_null() { let overlay = Mapping::from_str(overlay).unwrap(); p.merge(&overlay).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); let merged_some = r#" foo: @@ -345,7 +355,7 @@ fn test_merge_null() { let overlay = Mapping::from_str(overlay).unwrap(); p.merge(&overlay).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); let merged_some = r#" foo: null"#; @@ -391,7 +401,7 @@ fn test_merge_interpolate_embedded_nested_ref() { "#; let config2 = Mapping::from_str(config2).unwrap(); p.merge(&config2).unwrap(); - p = p.interpolate(&p).unwrap(); + p = p.render(&p).unwrap(); let val = p .get(&"foo".into()) @@ -402,3 +412,159 @@ fn test_merge_interpolate_embedded_nested_ref() { .unwrap(); assert_eq!(val, &Value::Literal("baz-foo-1.22".into())); } + +#[test] +fn test_interpolate_duplicate_ref_no_loop() { + let base = r#" + foo: + bar: ${baz}-${baz} + baz: baz + "#; + let base = Mapping::from_str(base).unwrap(); + + let p = base.render(&base).unwrap(); + + let expected = Mapping::from_str("{foo: {bar: baz-baz}, baz: baz}").unwrap(); + let expected = expected.render(&Mapping::new()).unwrap(); + assert_eq!(p, expected); +} + +#[test] +fn test_interpolate_sequence_duplicate_ref_no_loop() { + let base = r#" + foo: + bar: + - ${baz} + - ${baz} + baz: baz + "#; + let base = Mapping::from_str(base).unwrap(); + + let p = base.render(&base).unwrap(); + + let expected = Mapping::from_str("{foo: {bar: [baz, baz]}, baz: baz}").unwrap(); + let expected = expected.render(&Mapping::new()).unwrap(); + assert_eq!(p, expected); +} + +#[test] +fn test_interpolate_nested_mapping_no_loop() { + let base = r#" + foo: + bar: + baz: ${foo:baz:bar} + qux: foo + baz: + bar: qux + qux: ${foo:bar:qux} + "#; + let base = Mapping::from_str(base).unwrap(); + + let p = base.render(&base).unwrap(); + + let expected = + Mapping::from_str("{foo: {bar: {baz: qux, qux: foo}, baz: {bar: qux, qux: foo}}}").unwrap(); + let expected = expected.render(&Mapping::new()).unwrap(); + assert_eq!(p, expected); +} + +#[test] +#[should_panic(expected = "While resolving references in \ + {\"foo\": {\"bar\": \"${bar}\"}, \"bar\": [{\"baz\": \"baz\", \"qux\": \"qux\"}, \ + {\"baz\": \"${foo}\"}]}: Reference loop with reference paths [\"bar\", \"foo\"].")] +fn test_merge_interpolate_loop() { + let base = r#" + foo: + bar: ${bar} + bar: + baz: baz + qux: qux + "#; + let base = Mapping::from_str(base).unwrap(); + let config1 = r#" + bar: + baz: ${foo} + "#; + let config1 = Mapping::from_str(config1).unwrap(); + + let mut p = Mapping::new(); + p.merge(&base).unwrap(); + p.merge(&config1).unwrap(); + + let mut v = Value::from(p); + v.render_with_self().unwrap(); +} + +#[test] +#[should_panic(expected = "While resolving references in \ + {\"foo\": {\"bar\": [\"${bar}\", \"${baz}\"]}, \"bar\": \"${qux}\", \ + \"baz\": {\"bar\": \"${foo}\"}, \"qux\": 3.14}: \ + Reference loop with reference paths [\"baz\", \"foo\"].")] +fn test_interpolate_sequence_loop() { + let base = r#" + foo: + bar: + - ${bar} + - ${baz} + bar: ${qux} + baz: + bar: ${foo} + qux: 3.14 + "#; + let base = Mapping::from_str(base).unwrap(); + + let mut v = Value::from(base); + v.render_with_self().unwrap(); +} + +#[test] +#[should_panic(expected = "While resolving references in \ + {\"foo\": {\"bar\": {\"baz\": \"${foo:baz:bar}\", \"qux\": \"${foo:qux:foo}\"}, \ + \"baz\": {\"bar\": \"qux\", \"qux\": \"${foo:bar:qux}\"}, \"qux\": \ + {\"foo\": \"${foo:baz:qux}\"}}}: \ + Reference loop with reference paths [\"foo:bar:qux\", \"foo:baz:qux\", \"foo:qux:foo\"].")] +fn test_interpolate_nested_mapping_loop() { + let m = r#" + foo: + bar: + baz: ${foo:baz:bar} + qux: ${foo:qux:foo} + baz: + bar: qux + qux: ${foo:bar:qux} + qux: + foo: ${foo:baz:qux} + "#; + let m = Mapping::from_str(m).unwrap(); + + let mut v = Value::from(m); + v.render_with_self().unwrap(); +} + +#[test] +#[should_panic( + expected = "While resolving references in \"${foo:${foo:${foo:${foo:${foo:\ + ${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:\ + ${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:\ + ${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:\ + ${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:\ + ${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:${foo:\ + ${foo:${foo:${foo:${foo:${foo:${foo}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}\ + }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}\": \ + Token resolution exceeded recursion depth of 64. \ + We've seen the following reference paths: []." +)] +fn test_interpolate_depth_exceeded() { + // construct a reference string which is a nested sequence of ${foo:....${foo}} with 70 nesting + // levels. Note that the expected error has an empty list of reference paths because we hit the + // recursion limit before we even manage to construct the initial ref path in + // `Token::resolve()`. + let refstr = (0..70).fold("${foo}".to_string(), |s, _| format!("${{foo:{s}}}")); + let map = (0..70).fold(Mapping::from_str("foo: bar").unwrap(), |m, _| { + let mut n = Mapping::new(); + n.insert("foo".into(), Value::Mapping(m)).unwrap(); + n + }); + let v = Value::from(refstr); + v.rendered(&map).unwrap(); +}