Skip to content

Commit

Permalink
(fix): Sub variables only saw References
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
iph committed Mar 22, 2023
1 parent 2b6393b commit bff2f91
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 16 deletions.
Empty file added lfis.json
Empty file.
6 changes: 2 additions & 4 deletions src/ir/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
})
Expand Down Expand Up @@ -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();
Expand Down
45 changes: 35 additions & 10 deletions src/ir/sub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,6 @@ pub enum SubValue {

pub fn sub_parse_tree(str: &str) -> Result<Vec<SubValue>, 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)) => {
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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(())
}
}
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,6 +50,8 @@ pub struct CloudformationParseTree {
pub conditions: ConditionsParseTree,
pub resources: ResourcesParseTree,
pub outputs: OutputsParseTree,

logical_lookup: HashSet<String>,
}

impl CloudformationParseTree {
Expand All @@ -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)?,
Expand All @@ -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)
}
}
12 changes: 10 additions & 2 deletions src/synthesizer/typescript_synthesizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,11 +354,19 @@ pub fn to_string_ir(resource_value: &ResourceIr) -> Option<String> {
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();
Expand Down

0 comments on commit bff2f91

Please sign in to comment.