use super::context::TemplateContext;
use super::parser::TemplateNode;
pub async fn resolve_ast(
nodes: &[TemplateNode],
context: &mut dyn TemplateContext,
depth: usize,
max_depth: usize,
max_size: usize,
) -> String {
resolve_ast_with_depth(nodes, context, depth, max_depth, max_size).await
}
fn resolve_ast_with_depth<'a>(
nodes: &'a [TemplateNode],
context: &'a mut dyn TemplateContext,
depth: usize,
max_depth: usize,
max_size: usize,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + 'a>> {
Box::pin(async move {
if depth > max_depth {
fancy_log::log(
fancy_log::LogLevel::Error,
&format!("✗ Template recursion depth limit ({max_depth}) exceeded"),
);
return String::new();
}
let mut result = String::new();
for node in nodes {
match node {
TemplateNode::Text(s) => {
if result.len() + s.len() > max_size {
fancy_log::log(
fancy_log::LogLevel::Error,
&format!("✗ Template result size limit ({max_size}) exceeded"),
);
return result;
}
result.push_str(s);
}
TemplateNode::Variable { parts } => {
let key = resolve_ast_with_depth(parts, context, depth + 1, max_depth, max_size).await;
if key.contains('{') || key.contains('}') {
fancy_log::log(
fancy_log::LogLevel::Error,
&format!(
"✗ Security: Template injection attempt detected in key name: '{key}'. Refusing lookup."
),
);
result.push_str("{{");
result.push_str(&key);
result.push_str("}}");
continue;
}
let value = context.get(&key).await;
if result.len() + value.len() > max_size {
fancy_log::log(
fancy_log::LogLevel::Error,
&format!("✗ Template result size limit ({max_size}) exceeded"),
);
return result;
}
result.push_str(&value);
}
}
}
result
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::kv::KvStore;
use crate::resources::templates::context::SimpleContext;
use crate::resources::templates::parser::parse_template;
#[tokio::test]
async fn test_resolve_simple() {
let mut kv = KvStore::new();
kv.insert("key".to_string(), "value".to_string());
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("{{key}}").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "value");
}
#[tokio::test]
async fn test_resolve_concatenation() {
let mut kv = KvStore::new();
kv.insert("conn.ip".to_string(), "1.2.3.4".to_string());
kv.insert("conn.port".to_string(), "8080".to_string());
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("{{conn.ip}}:{{conn.port}}").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "1.2.3.4:8080");
}
#[tokio::test]
async fn test_resolve_nested() {
let mut kv = KvStore::new();
kv.insert("conn.protocol".to_string(), "http".to_string());
kv.insert("kv.http_backend".to_string(), "backend-01".to_string());
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("{{kv.{{conn.protocol}}_backend}}").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "backend-01");
}
#[tokio::test]
async fn test_resolve_complex() {
let mut kv = KvStore::new();
kv.insert("geo.country".to_string(), "US".to_string());
kv.insert("kv.US_domain".to_string(), "api.example.com".to_string());
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("https://{{kv.{{geo.country}}_domain}}/api").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "https://api.example.com/api");
}
#[tokio::test]
async fn test_resolve_missing_key() {
let mut kv = KvStore::new();
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("{{missing}}").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "{{missing}}");
}
#[tokio::test]
async fn test_resolve_empty() {
let mut kv = KvStore::new();
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let result = resolve_ast(&[], &mut context, 0, 5, 65536).await;
assert_eq!(result, "");
}
#[tokio::test]
async fn test_resolve_plain_text() {
let mut kv = KvStore::new();
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("plain text").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "plain text");
}
#[tokio::test]
async fn test_resolve_injection_attempt() {
let mut kv = KvStore::new();
kv.insert("user_input".to_string(), "{{system.token}}".to_string());
kv.insert("system.token".to_string(), "SECRET".to_string());
let mut context = SimpleContext {
kv: &mut kv,
payloads: None,
};
let ast = parse_template("{{prefix.{{user_input}}}}").unwrap();
let result = resolve_ast(&ast, &mut context, 0, 5, 65536).await;
assert_eq!(result, "{{prefix.{{system.token}}}}");
}
}