use super::*;
pub(super) fn stringify_value(value: &Value, parameters: &BTreeMap<String, String>) -> String {
match value {
Value::String(s) => s.clone(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::Object(m) => {
if let Some(name) = m.get("Ref").and_then(|v| v.as_str()) {
if let Some(p) = parameters.get(name) {
return p.clone();
}
return name.to_string();
}
value.to_string()
}
_ => value.to_string(),
}
}
pub(super) fn pseudo_value(name: &str, parameters: &BTreeMap<String, String>) -> Option<Value> {
if name == "AWS::NotificationARNs" {
if let Some(raw) = parameters.get(name) {
if let Ok(parsed) = serde_json::from_str::<Vec<String>>(raw) {
return Some(Value::Array(
parsed.into_iter().map(Value::String).collect(),
));
}
}
return Some(Value::Array(Vec::new()));
}
if let Some(v) = parameters.get(name) {
return Some(Value::String(v.clone()));
}
let region = parameters
.get("AWS::Region")
.map(String::as_str)
.unwrap_or("us-east-1");
match name {
"AWS::Partition" => Some(Value::String(partition_for_region(region).to_string())),
"AWS::URLSuffix" => Some(Value::String(url_suffix_for_region(region).to_string())),
"AWS::Region" => Some(Value::String(region.to_string())),
"AWS::NoValue" => Some(no_value_sentinel()),
_ => None,
}
}
pub(super) fn no_value_sentinel() -> Value {
let mut m = serde_json::Map::new();
m.insert(NO_VALUE_SENTINEL_KEY.to_string(), Value::Bool(true));
Value::Object(m)
}
pub(super) fn is_no_value(value: &Value) -> bool {
value
.as_object()
.map(|m| m.len() == 1 && m.contains_key(NO_VALUE_SENTINEL_KEY))
.unwrap_or(false)
}
pub(super) fn strip_no_value(value: Value) -> Value {
match value {
Value::Object(map) => {
if is_no_value(&Value::Object(map.clone())) {
return Value::Null;
}
let mut out = serde_json::Map::with_capacity(map.len());
for (k, v) in map {
if is_no_value(&v) {
continue;
}
out.insert(k, strip_no_value(v));
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(
arr.into_iter()
.filter(|v| !is_no_value(v))
.map(strip_no_value)
.collect(),
),
other => other,
}
}
#[cfg(test)]
pub(super) fn resolve_refs(
value: &Value,
parameters: &BTreeMap<String, String>,
_resources: &serde_json::Map<String, Value>,
resource_physical_ids: &BTreeMap<String, String>,
resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
) -> Value {
resolve_refs_full(
value,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
&BTreeMap::new(),
&BTreeMap::new(),
)
}
pub(super) fn resolve_refs_full(
value: &Value,
parameters: &BTreeMap<String, String>,
_resources: &serde_json::Map<String, Value>,
resource_physical_ids: &BTreeMap<String, String>,
resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
imports: &BTreeMap<String, String>,
conditions: &BTreeMap<String, bool>,
) -> Value {
if let Some(map) = value.as_object() {
if let Some(arr) = map.get("Fn::If").and_then(|v| v.as_array()) {
if arr.len() == 3 {
let cond_name = arr[0].as_str().unwrap_or("");
let picked = if conditions.get(cond_name).copied().unwrap_or(false) {
&arr[1]
} else {
&arr[2]
};
return resolve_refs_full(
picked,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
}
}
}
match value {
Value::Object(map) => {
if let Some(ref_val) = map.get("Ref") {
if let Some(ref_name) = ref_val.as_str() {
if PSEUDO_REFS.contains(&ref_name) {
if let Some(v) = pseudo_value(ref_name, parameters) {
return v;
}
return Value::String(ref_name.to_string());
}
if let Some(param_val) = parameters.get(ref_name) {
return Value::String(param_val.clone());
}
if let Some(physical_id) = resource_physical_ids.get(ref_name) {
return Value::String(physical_id.clone());
}
if _resources.contains_key(ref_name) {
return Value::String(ref_name.to_string());
}
return Value::String(ref_name.to_string());
}
}
if let Some(import_val) = map.get("Fn::ImportValue") {
let resolved = resolve_refs_full(
import_val,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let key = match &resolved {
Value::String(s) => s.clone(),
other => other.to_string(),
};
if let Some(v) = imports.get(&key) {
return Value::String(v.clone());
}
return Value::String(String::new());
}
if let Some(getatt_val) = map.get("Fn::GetAtt") {
if let Some((logical_id, attr_name)) = parse_getatt(getatt_val) {
if let Some(attrs) = resource_attributes.get(&logical_id) {
if let Some(attr_value) = attrs.get(&attr_name) {
return Value::String(attr_value.clone());
}
}
return Value::String(format!("{logical_id}.{attr_name}"));
}
}
if let Some(join_val) = map.get("Fn::Join") {
if let Some(arr) = join_val.as_array() {
if arr.len() == 2 {
let delimiter = arr[0].as_str().unwrap_or("");
if let Some(parts) = arr[1].as_array() {
let resolved_parts: Vec<String> = parts
.iter()
.map(|p| {
let resolved = resolve_refs_full(
p,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
match resolved {
Value::String(s) => s,
other => other.to_string(),
}
})
.collect();
return Value::String(resolved_parts.join(delimiter));
}
}
}
}
if let Some(b64_val) = map.get("Fn::Base64") {
let resolved = resolve_refs_full(
b64_val,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let s = match &resolved {
Value::String(s) => s.clone(),
other => other.to_string(),
};
return Value::String(
base64::engine::general_purpose::STANDARD.encode(s.as_bytes()),
);
}
if let Some(len_val) = map.get("Fn::Length") {
let resolved = resolve_refs_full(
len_val,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let n: usize = match &resolved {
Value::Array(arr) => arr.len(),
Value::String(s) => s.chars().count(),
_ => 0,
};
return Value::Number(serde_json::Number::from(n));
}
if let Some(to_json) = map.get("Fn::ToJsonString") {
let resolved = resolve_refs_full(
to_json,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let s = serde_json::to_string(&resolved).unwrap_or_default();
return Value::String(s);
}
if let Some(split_val) = map.get("Fn::Split") {
if let Some(arr) = split_val.as_array() {
if arr.len() == 2 {
let delim = arr[0].as_str().unwrap_or("");
let src_resolved = resolve_refs_full(
&arr[1],
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let src = match src_resolved {
Value::String(s) => s,
other => other.to_string(),
};
let parts: Vec<Value> = src
.split(delim)
.map(|p| Value::String(p.to_string()))
.collect();
return Value::Array(parts);
}
}
}
if let Some(sel_val) = map.get("Fn::Select") {
if let Some(arr) = sel_val.as_array() {
if arr.len() == 2 {
let idx_val = resolve_refs_full(
&arr[0],
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let list_val = resolve_refs_full(
&arr[1],
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let idx: usize = match &idx_val {
Value::Number(n) => n.as_u64().unwrap_or(0) as usize,
Value::String(s) => s.parse().unwrap_or(0),
_ => 0,
};
if let Some(list) = list_val.as_array() {
if let Some(elt) = list.get(idx) {
return elt.clone();
}
}
return Value::Null;
}
}
}
if let Some(cidr_val) = map.get("Fn::Cidr") {
if let Some(arr) = cidr_val.as_array() {
if arr.len() == 3 {
let block_val = resolve_refs_full(
&arr[0],
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let count_val = resolve_refs_full(
&arr[1],
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let bits_val = resolve_refs_full(
&arr[2],
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let block_str = match &block_val {
Value::String(s) => s.clone(),
other => other.to_string(),
};
let count: u32 = match &count_val {
Value::Number(n) => n.as_u64().unwrap_or(0) as u32,
Value::String(s) => s.parse().unwrap_or(0),
_ => 0,
};
let cidr_bits: u32 = match &bits_val {
Value::Number(n) => n.as_u64().unwrap_or(0) as u32,
Value::String(s) => s.parse().unwrap_or(0),
_ => 0,
};
if let Some(sub_cidrs) = compute_cidr_subnets(&block_str, count, cidr_bits)
{
return Value::Array(
sub_cidrs.into_iter().map(Value::String).collect(),
);
}
}
}
}
if let Some(sub_val) = map.get("Fn::Sub") {
let (template_str, extra_vars): (Option<&str>, BTreeMap<String, String>) =
if let Some(s) = sub_val.as_str() {
(Some(s), BTreeMap::new())
} else if let Some(arr) = sub_val.as_array() {
let str_part = arr.first().and_then(|v| v.as_str());
let mut bindings: BTreeMap<String, String> = BTreeMap::new();
if let Some(obj) = arr.get(1).and_then(|v| v.as_object()) {
for (k, v) in obj {
let resolved = resolve_refs_full(
v,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
);
let s = match resolved {
Value::String(s) => s,
other => other.to_string(),
};
bindings.insert(k.clone(), s);
}
}
(str_part, bindings)
} else {
(None, BTreeMap::new())
};
if let Some(s) = template_str {
let mut result = s.to_string();
for (k, v) in &extra_vars {
result = result.replace(&format!("${{{k}}}"), v);
}
for pseudo in PSEUDO_REFS {
let token = format!("${{{pseudo}}}");
if !result.contains(&token) {
continue;
}
if *pseudo == "AWS::NoValue" {
result = result.replace(&token, "");
continue;
}
if let Some(v) = pseudo_value(pseudo, parameters) {
let s = match v {
Value::String(s) => s,
other => other.to_string(),
};
result = result.replace(&token, &s);
}
}
for (k, v) in parameters {
result = result.replace(&format!("${{{k}}}"), v);
}
for (k, v) in resource_physical_ids {
result = result.replace(&format!("${{{k}}}"), v);
}
for (logical, attrs) in resource_attributes {
for (attr, value) in attrs {
result = result.replace(&format!("${{{logical}.{attr}}}"), value);
}
}
return Value::String(result);
}
}
let mut new_map = serde_json::Map::new();
for (k, v) in map {
new_map.insert(
k.clone(),
resolve_refs_full(
v,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
),
);
}
Value::Object(new_map)
}
Value::Array(arr) => Value::Array(
arr.iter()
.map(|v| {
resolve_refs_full(
v,
parameters,
_resources,
resource_physical_ids,
resource_attributes,
imports,
conditions,
)
})
.collect(),
),
other => other.clone(),
}
}
pub(super) fn compute_cidr_subnets(
ip_block: &str,
count: u32,
cidr_bits: u32,
) -> Option<Vec<String>> {
if !(1..=256).contains(&count) {
return None;
}
let (ip_str, prefix_str) = ip_block.split_once('/')?;
let prefix: u32 = prefix_str.parse().ok()?;
let ip: std::net::Ipv4Addr = ip_str.parse().ok()?;
let base: u32 = ip.into();
let new_prefix = 32u32.checked_sub(cidr_bits)?;
if new_prefix <= prefix {
return None;
}
let step: u32 = 1u32 << cidr_bits;
let mut out = Vec::with_capacity(count as usize);
for i in 0..count {
let subnet_base = base.checked_add(step.checked_mul(i)?)?;
let addr = std::net::Ipv4Addr::from(subnet_base);
out.push(format!("{addr}/{new_prefix}"));
}
Some(out)
}
pub(super) fn parse_getatt(value: &Value) -> Option<(String, String)> {
match value {
Value::Array(arr) if arr.len() >= 2 => {
let logical_id = arr[0].as_str()?.to_string();
let parts: Vec<String> = arr[1..]
.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.collect();
Some((logical_id, parts.join(".")))
}
Value::String(s) => {
let (logical_id, attr) = s.split_once('.')?;
Some((logical_id.to_string(), attr.to_string()))
}
_ => None,
}
}