From bff2f9111a1049c320f1ea53087e1da95efe8b1f Mon Sep 17 00:00:00 2001 From: Sean Tyler Myers Date: Tue, 21 Mar 2023 22:02:25 -0700 Subject: [PATCH] (fix): Sub variables only saw References So, when the Sub parser goes through, you can get a blob like: ``` "Fn::Sub": "echo ${lol}" ``` which would transform lol into a variable, as it's surrounded by ${}. It turns out that CFN doesn't need a reference of any kind, and will just output the string inside if it doesn't match any origin. Meanwhile, my system would attempt to topological sort `lol` off a Logical Id, but not find a Logical Id! So it would crash. There were a few other bugs in this patch (escaping ticks, simplify the parser) that aren't worth mentioning. --- lfis.json | 0 src/ir/resources.rs | 6 +-- src/ir/sub.rs | 45 ++++++++++++++++++----- src/lib.rs | 13 +++++++ src/synthesizer/typescript_synthesizer.rs | 12 +++++- 5 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 lfis.json diff --git a/lfis.json b/lfis.json new file mode 100644 index 00000000..e69de29b diff --git a/src/ir/resources.rs b/src/ir/resources.rs index f87a6af0..17bca585 100644 --- a/src/ir/resources.rs +++ b/src/ir/resources.rs @@ -532,10 +532,7 @@ pub fn translate_resource( .map(|x| match &x { SubValue::String(x) => ResourceIr::String(x.to_string()), SubValue::Variable(x) => match excess_map.get(x) { - None => { - // if x has a period, it is actually a get-attr - ResourceIr::Ref(find_ref(x, resource_translator.parse_tree)) - } + None => ResourceIr::Ref(find_ref(x, resource_translator.parse_tree)), Some(x) => x.clone(), }, }) @@ -668,6 +665,7 @@ fn find_ref(x: &str, parse_tree: &CloudformationParseTree) -> Reference { } } + // if x has a period, it is actually a get-attr if x.contains('.') { let splits = x.split('.'); let sp: Vec<&str> = splits.collect(); diff --git a/src/ir/sub.rs b/src/ir/sub.rs index d7b1d7fe..2a8d7f28 100644 --- a/src/ir/sub.rs +++ b/src/ir/sub.rs @@ -16,16 +16,6 @@ pub enum SubValue { pub fn sub_parse_tree(str: &str) -> Result, TransmuteError> { let mut full_resolver = many1(inner_resolver); - // Remove the beginning and ending quotations, as they are annoying to add to the parse tree. - // TODO - just add them to the parse grammar to simplify this. - let str = match str.strip_prefix('\"') { - None => str, - Some(x) => x, - }; - let str = match str.strip_suffix('\"') { - None => str, - Some(x) => x, - }; match full_resolver(str) { Ok((remaining, built_subs)) => { @@ -57,6 +47,10 @@ fn inner_resolver(str: &str) -> IResult<&str, SubValue> { } let ir = alt(( + map( + delimited(tag("${!"), take_until("}"), take(1usize)), + |var: &str| SubValue::String(format!("${{{var}}}")), + ), // Attempt to find ${...} and eat those tokens. map( delimited(tag("${"), take_until("}"), take(1usize)), @@ -142,4 +136,35 @@ mod tests { Ok(()) } + + #[test] + fn test_quotes() -> Result<(), TransmuteError> { + let v = sub_parse_tree("echo \"${lol}\"")?; + assert_eq!( + v, + vec![ + SubValue::String(String::from("echo \"")), + SubValue::Variable(String::from("lol")), + SubValue::String(String::from("\"")), + ] + ); + + Ok(()) + } + + // As quoted in the sub docs: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html + // To write a dollar sign and curly braces (${}) literally, add an exclamation point (!) after the open curly brace, such as ${!Literal}. CloudFormation resolves this text as ${Literal}. + #[test] + fn test_literal() -> Result<(), TransmuteError> { + let v = sub_parse_tree("echo ${!lol}")?; + assert_eq!( + v, + vec![ + SubValue::String(String::from("echo ")), + SubValue::String(String::from("${lol}")) + ] + ); + + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index d41751fc..631e2819 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use crate::parser::output::{build_outputs, OutputsParseTree}; use crate::parser::parameters::{build_parameters, Parameters}; use crate::parser::resource::{build_resources, ResourceValue, ResourcesParseTree}; use serde_json::Value; +use std::collections::HashSet; pub mod integrations; pub mod ir; @@ -49,6 +50,8 @@ pub struct CloudformationParseTree { pub conditions: ConditionsParseTree, pub resources: ResourcesParseTree, pub outputs: OutputsParseTree, + + logical_lookup: HashSet, } impl CloudformationParseTree { @@ -66,6 +69,11 @@ impl CloudformationParseTree { // All stacks must have resources, so no checking. let resources = build_resources(json_obj["Resources"].as_object().unwrap())?; + let mut logical_lookup = HashSet::new(); + for resource in resources.resources.iter() { + logical_lookup.insert(resource.name.clone()); + } + let mappings = match json_obj["Mappings"].as_object() { None => MappingsParseTree::new(), Some(x) => build_mappings(x)?, @@ -81,6 +89,11 @@ impl CloudformationParseTree { resources, mappings, outputs, + logical_lookup, }) } + + pub fn contains_logical_id(&self, logical_id: &str) -> bool { + self.logical_lookup.contains(logical_id) + } } diff --git a/src/synthesizer/typescript_synthesizer.rs b/src/synthesizer/typescript_synthesizer.rs index 5870a0bb..4fff841d 100644 --- a/src/synthesizer/typescript_synthesizer.rs +++ b/src/synthesizer/typescript_synthesizer.rs @@ -354,11 +354,19 @@ pub fn to_string_ir(resource_value: &ResourceIr) -> Option { let mut r = Vec::new(); for i in arr.iter() { match i { - ResourceIr::String(s) => r.push(s.to_string()), + ResourceIr::String(s) => { + // Since we are changing the output strings to use ticks for typescript sugar syntax, + // we need to escape the ticks that already exist. + let _replaced = s.replace('`', "\\`"); + let _replaced = s.replace('{', "\\{`"); + let replaced = s.replace('}', "\\}`"); + r.push(replaced.to_string()) + } &_ => r.push(format!("${{{}}}", to_string_ir(i).unwrap())), }; } - Option::Some(format!("`{}`", r.join(""))) + let full_text = r.join(""); + Option::Some(format!("`{full_text}`")) } ResourceIr::Map(mapper, first, second) => { let a: &ResourceIr = mapper.as_ref();