From ba2ce74d19ac45c4071cea46baa25deff7f98cf3 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Wed, 10 Jul 2024 04:53:26 +0000 Subject: [PATCH 01/15] feat: add basic async support --- Cargo.toml | 3 ++- src/renderer/mod.rs | 17 +++++++++++++++++ src/renderer/processor.rs | 14 ++++++++++++++ src/tera.rs | 9 +++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b13f614c..843ea556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,8 @@ pretty_assertions = "1" tempfile = "3" [features] -default = ["builtins"] +default = ["builtins", "async"] +async = [] builtins = ["urlencode", "slug", "humansize", "chrono", "chrono-tz", "rand"] urlencode = ["percent-encoding"] preserve_order = ["serde_json/preserve_order"] diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 3f01e153..6b95e5a7 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -52,6 +52,14 @@ impl<'a> Renderer<'a> { buffer_to_string(|| "converting rendered buffer to string".to_string(), output) } + /// Async version of [`render()`](Self::render) + #[cfg(feature = "async")] + pub async fn render_async(&self) -> Result { + let mut output = Vec::with_capacity(2000); + self.render_to_async(&mut output).await?; + buffer_to_string(|| "converting rendered buffer to string".to_string(), output) + } + /// Combines the context with the Template to write the end result to output pub fn render_to(&self, mut output: impl Write) -> Result<()> { let mut processor = @@ -59,4 +67,13 @@ impl<'a> Renderer<'a> { processor.render(&mut output) } + + /// Async version of [`render_to()`](Self::render_to) + #[cfg(feature = "async")] + pub async fn render_to_async(&self, mut output: impl Write) -> Result<()> { + let mut processor = + Processor::new(self.template, self.tera, self.context, self.should_escape); + + processor.render_async(&mut output).await + } } diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index 56c6c486..5cf5673d 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -1063,4 +1063,18 @@ impl<'a> Processor<'a> { Ok(()) } + + /// Async version of [`render`](Self::render) + #[cfg(feature = "async")] + pub async fn render_async(&mut self, write: &mut impl Write) -> Result<()> { + for node in &self.template_root.ast { + self.render_node(node, write) + .map_err(|e| Error::chain(self.get_error_location(), e))?; + + // await after each node to allow the runtime to yield to other tasks + async {}.await; + } + + Ok(()) + } } diff --git a/src/tera.rs b/src/tera.rs index 0c694f5c..9f25414a 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -392,6 +392,15 @@ impl Tera { renderer.render() } + /// Async version of [`render()`](Self::render). Note that the await points for ``render_async`` are + /// in between each ast parse + #[cfg(feature = "async")] + pub async fn render_async(&self, template_name: &str, context: &Context) -> Result { + let template = self.get_template(template_name)?; + let renderer = Renderer::new(template, self, context); + renderer.render_async().await + } + /// Renders a Tera template given a [`Context`] to something that implements [`Write`]. /// /// The only difference from [`render()`](Self::render) is that this version doesn't convert From 31576f8c523ecf4898f1a6c6581723282f130696 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Wed, 10 Jul 2024 04:55:51 +0000 Subject: [PATCH 02/15] fix: clippy lint while we're here --- src/renderer/processor.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index 5cf5673d..aea5cae2 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -1036,8 +1036,7 @@ impl<'a> Processor<'a> { // which template are we in? if let Some(&(name, _template, ref level)) = self.blocks.last() { - let block_def = - self.template.blocks_definitions.get(&name.to_string()).and_then(|b| b.get(*level)); + let block_def = self.template.blocks_definitions.get(name).and_then(|b| b.get(*level)); if let Some((tpl_name, _)) = block_def { if tpl_name != &self.template.name { From c3b3ab7235b159a189b17b48307b69198af5d04a Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Wed, 10 Jul 2024 05:20:08 +0000 Subject: [PATCH 03/15] remove: dangerous function get_env --- src/builtins/functions.rs | 52 --------------------------------------- src/tera.rs | 1 - 2 files changed, 53 deletions(-) diff --git a/src/builtins/functions.rs b/src/builtins/functions.rs index 9c8a1e6e..a2690e29 100644 --- a/src/builtins/functions.rs +++ b/src/builtins/functions.rs @@ -171,29 +171,6 @@ pub fn get_random(args: &HashMap) -> Result { Ok(Value::Number(res.into())) } -pub fn get_env(args: &HashMap) -> Result { - let name = match args.get("name") { - Some(val) => match from_value::(val.clone()) { - Ok(v) => v, - Err(_) => { - return Err(Error::msg(format!( - "Function `get_env` received name={} but `name` can only be a string", - val - ))); - } - }, - None => return Err(Error::msg("Function `get_env` didn't receive a `name` argument")), - }; - - match std::env::var(&name).ok() { - Some(res) => Ok(Value::String(res)), - None => match args.get("default") { - Some(default) => Ok(default.clone()), - None => Err(Error::msg(format!("Environment variable `{}` not found", &name))), - }, - } -} - #[cfg(test)] mod tests { use std::collections::HashMap; @@ -309,33 +286,4 @@ mod tests { assert!(res.as_i64().unwrap() >= 5); assert!(res.as_i64().unwrap() < 10); } - - #[test] - fn get_env_existing() { - std::env::set_var("TERA_TEST", "true"); - let mut args = HashMap::new(); - args.insert("name".to_string(), to_value("TERA_TEST").unwrap()); - let res = get_env(&args).unwrap(); - assert!(res.is_string()); - assert_eq!(res.as_str().unwrap(), "true"); - std::env::remove_var("TERA_TEST"); - } - - #[test] - fn get_env_non_existing_no_default() { - let mut args = HashMap::new(); - args.insert("name".to_string(), to_value("UNKNOWN_VAR").unwrap()); - let res = get_env(&args); - assert!(res.is_err()); - } - - #[test] - fn get_env_non_existing_with_default() { - let mut args = HashMap::new(); - args.insert("name".to_string(), to_value("UNKNOWN_VAR").unwrap()); - args.insert("default".to_string(), to_value("false").unwrap()); - let res = get_env(&args).unwrap(); - assert!(res.is_string()); - assert_eq!(res.as_str().unwrap(), "false"); - } } diff --git a/src/tera.rs b/src/tera.rs index 9f25414a..aab77574 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -760,7 +760,6 @@ impl Tera { self.register_function("throw", functions::throw); #[cfg(feature = "builtins")] self.register_function("get_random", functions::get_random); - self.register_function("get_env", functions::get_env); } /// Select which suffix(es) to automatically do HTML escaping on. From b66de89697fc3421ddf51bfc2c35d21594ed48ab Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Thu, 11 Jul 2024 05:02:41 +0000 Subject: [PATCH 04/15] fix: remove use of unreachable in favor of errors to avoid abuse/ddos --- src/builtins/filters/array.rs | 2 +- src/builtins/functions.rs | 5 +- src/context.rs | 5 +- src/parser/mod.rs | 325 ++++++++++++++++++++++++++++------ src/parser/whitespace.rs | 2 +- src/renderer/for_loop.rs | 2 +- src/renderer/processor.rs | 27 ++- src/renderer/tests/basic.rs | 8 +- 8 files changed, 302 insertions(+), 74 deletions(-) diff --git a/src/builtins/filters/array.rs b/src/builtins/filters/array.rs index f050a6a1..04c5b106 100644 --- a/src/builtins/filters/array.rs +++ b/src/builtins/filters/array.rs @@ -297,7 +297,7 @@ pub fn concat(value: &Value, args: &HashMap) -> Result { arr.push(val.clone()); } } - _ => unreachable!("Got something other than an array??"), + _ => return Err(Error::msg("The `concat` filter can only concat with an array")), } } else { arr.push(value.clone()); diff --git a/src/builtins/functions.rs b/src/builtins/functions.rs index a2690e29..ea9c79ca 100644 --- a/src/builtins/functions.rs +++ b/src/builtins/functions.rs @@ -1,3 +1,4 @@ +use crate::Tera; use std::collections::HashMap; #[cfg(feature = "builtins")] @@ -11,7 +12,7 @@ use crate::errors::{Error, Result}; /// The global function type definition pub trait Function: Sync + Send { /// The global function type definition - fn call(&self, args: &HashMap) -> Result; + fn call(&self, tera: &Tera, args: &HashMap) -> Result; /// Whether the current function's output should be treated as safe, defaults to `false` fn is_safe(&self) -> bool { @@ -23,7 +24,7 @@ impl Function for F where F: Fn(&HashMap) -> Result + Sync + Send, { - fn call(&self, args: &HashMap) -> Result { + fn call(&self, _tera: &Tera, args: &HashMap) -> Result { self(args) } } diff --git a/src/context.rs b/src/context.rs index 5a874ca4..a343338b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -152,7 +152,10 @@ impl ValueRender for Value { } else if let Some(v) = i.as_f64() { write!(write, "{}", v) } else { - unreachable!() + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Number could not be converted to i64, u64 or f64", + )) } } Value::Bool(i) => write!(write, "{}", i), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a4248ff3..5dffab08 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -62,7 +62,12 @@ fn parse_kwarg(pair: Pair) -> TeraResult<(String, Expr)> { Rule::ident => name = Some(p.as_span().as_str().to_string()), Rule::logic_expr => val = Some(parse_logic_expr(p)?), Rule::array_filter => val = Some(parse_array_with_filters(p)?), - _ => unreachable!("{:?} not supposed to get there (parse_kwarg)!", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Got {:?} in parse_kwarg", + p.as_rule() + ))) + } }; } @@ -80,7 +85,12 @@ fn parse_fn_call(pair: Pair) -> TeraResult { let (name, val) = parse_kwarg(p)?; args.insert(name, val); } - _ => unreachable!("{:?} not supposed to get there (parse_fn_call)!", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_fn_call", + p.as_rule() + ))) + } }; } @@ -100,7 +110,12 @@ fn parse_filter(pair: Pair) -> TeraResult { Rule::fn_call => { return parse_fn_call(p); } - _ => unreachable!("{:?} not supposed to get there (parse_filter)!", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_filter", + p.as_rule() + ))) + } }; } @@ -125,11 +140,21 @@ fn parse_test_call(pair: Pair) -> TeraResult<(String, Vec)> { Rule::array => { args.push(Expr::new(parse_array(p2)?)); } - _ => unreachable!("Invalid arg type for test {:?}", p2.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Invalid arg type for test {:?} in parse_test_call", + p2.as_rule() + ))) + } } } } - _ => unreachable!("{:?} not supposed to get there (parse_test_call)!", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_test_call", + p.as_rule() + ))) + } }; } @@ -149,7 +174,12 @@ fn parse_test(pair: Pair) -> TeraResult { name = Some(_name); args = _args; } - _ => unreachable!("{:?} not supposed to get there (parse_ident)!", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_test/parse_ident", + p.as_rule() + ))) + } }; } @@ -200,7 +230,12 @@ fn parse_string_concat(pair: Pair) -> TeraResult { } values.push(ExprVal::FunctionCall(parse_fn_call(p)?)) } - _ => unreachable!("Got {:?} in parse_string_concat", p), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_string_concat", + p.as_rule() + ))) + } }; } @@ -228,7 +263,9 @@ fn parse_basic_expression(pair: Pair) -> TeraResult { Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, - _ => unreachable!(), + _ => { + return Err(Error::msg(format!("Unexpected rule in infix: {:?}", op.as_rule()))) + } }, rhs: Box::new(Expr::new(rhs?)), })) @@ -250,7 +287,7 @@ fn parse_basic_expression(pair: Pair) -> TeraResult { "True" => ExprVal::Bool(true), "false" => ExprVal::Bool(false), "False" => ExprVal::Bool(false), - _ => unreachable!(), + _ => return Err(Error::msg("PARSER ERROR: Unexpected boolean value")), }, Rule::test => ExprVal::Test(parse_test(pair)?), Rule::test_not => { @@ -264,7 +301,12 @@ fn parse_basic_expression(pair: Pair) -> TeraResult { Rule::basic_expr => { MATH_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } - _ => unreachable!("Got {:?} in parse_basic_expression: {}", pair.as_rule(), pair.as_str()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unexpected rule in parse_basic_expression: {:?}", + pair.as_rule() + ))) + } }; Ok(expr) } @@ -278,7 +320,12 @@ fn parse_basic_expr_with_filters(pair: Pair) -> TeraResult { match p.as_rule() { Rule::basic_expr => expr_val = Some(parse_basic_expression(p)?), Rule::filter => filters.push(parse_filter(p)?), - _ => unreachable!("Got {:?}", p), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_basic_expr_with_filters", + p + ))) + } }; } @@ -295,7 +342,12 @@ fn parse_string_expr_with_filters(pair: Pair) -> TeraResult { Rule::string => expr_val = Some(ExprVal::String(replace_string_markers(p.as_str()))), Rule::string_concat => expr_val = Some(parse_string_concat(p)?), Rule::filter => filters.push(parse_filter(p)?), - _ => unreachable!("Got {:?}", p), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_string_expr_with_filters", + p + ))) + } }; } @@ -311,7 +363,12 @@ fn parse_array_with_filters(pair: Pair) -> TeraResult { match p.as_rule() { Rule::array => array = Some(parse_array(p)?), Rule::filter => filters.push(parse_filter(p)?), - _ => unreachable!("Got {:?}", p), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_array_with_filters", + p + ))) + } }; } @@ -327,7 +384,12 @@ fn parse_in_condition_container(pair: Pair) -> TeraResult { expr = Some(Expr::new(ExprVal::Ident(p.as_str().to_string()))) } Rule::string_expr_filter => expr = Some(parse_string_expr_with_filters(p)?), - _ => unreachable!("Got {:?} in parse_in_condition_container", p), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_in_condition_container", + p + ))) + } }; } Ok(expr.unwrap()) @@ -346,7 +408,12 @@ fn parse_in_condition(pair: Pair) -> TeraResult { // rhs Rule::in_cond_container => rhs = Some(parse_in_condition_container(p)?), Rule::op_not => negated = true, - _ => unreachable!("Got {:?} in parse_in_condition", p), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_in_condition", + p + ))) + } }; } @@ -370,7 +437,12 @@ fn parse_comparison_val(pair: Pair) -> TeraResult { Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unexpected rule in infix: {:?}", + op + ))) + } }, rhs: Box::new(rhs?), }))) @@ -381,7 +453,12 @@ fn parse_comparison_val(pair: Pair) -> TeraResult { Rule::comparison_val => { MATH_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } - _ => unreachable!("Got {:?} in parse_comparison_val", pair.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_comparison_val", + pair.as_rule() + ))) + } }; Ok(expr) } @@ -399,7 +476,12 @@ fn parse_comparison_expression(pair: Pair) -> TeraResult { Rule::op_gte => LogicOperator::Gte, Rule::op_ineq => LogicOperator::NotEq, Rule::op_eq => LogicOperator::Eq, - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unexpected rule in infix: {:?}", + op + ))) + } }, rhs: Box::new(rhs?), }))) @@ -411,7 +493,12 @@ fn parse_comparison_expression(pair: Pair) -> TeraResult { Rule::comparison_expr => { COMPARISON_EXPR_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } - _ => unreachable!("Got {:?} in parse_comparison_expression", pair.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_comparison_expression", + pair.as_rule() + ))) + } }; Ok(expr) } @@ -428,7 +515,12 @@ fn parse_logic_val(pair: Pair) -> TeraResult { Rule::comparison_expr => expr = Some(parse_comparison_expression(p)?), Rule::string_expr_filter => expr = Some(parse_string_expr_with_filters(p)?), Rule::logic_expr => expr = Some(parse_logic_expr(p)?), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_logic_val", + p.as_rule() + ))) + } }; } @@ -451,10 +543,7 @@ fn parse_logic_expr(pair: Pair) -> TeraResult { operator: LogicOperator::And, rhs: Box::new(rhs?), }))), - _ => unreachable!( - "{:?} not supposed to get there (infix of logic_expression)!", - op.as_rule() - ), + _ => Err(Error::msg(format!("PARSER ERROR: Unexpected rule in infix: {:?}", op.as_rule()))), }; let expr = match pair.as_rule() { @@ -462,7 +551,12 @@ fn parse_logic_expr(pair: Pair) -> TeraResult { Rule::logic_expr => { LOGIC_EXPR_PARSER.map_primary(primary).map_infix(infix).parse(pair.into_inner())? } - _ => unreachable!("Got {:?} in parse_logic_expr", pair.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_logic_expr", + pair.as_rule() + ))) + } }; Ok(expr) } @@ -475,7 +569,12 @@ fn parse_array(pair: Pair) -> TeraResult { Rule::logic_val => { vals.push(parse_logic_val(p)?); } - _ => unreachable!("Got {:?} in parse_array", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_array", + p.as_rule() + ))) + } } } @@ -490,7 +589,7 @@ fn parse_string_array(pair: Pair) -> Vec { Rule::string => { vals.push(replace_string_markers(p.as_span().as_str())); } - _ => unreachable!("Got {:?} in parse_string_array", p.as_rule()), + _ => continue, } } @@ -516,7 +615,12 @@ fn parse_macro_call(pair: Pair) -> TeraResult { let (key, val) = parse_kwarg(p)?; args.insert(key, val); } - _ => unreachable!("Got {:?} in parse_macro_call", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_macro_call", + p.as_rule() + ))) + } } } @@ -537,7 +641,12 @@ fn parse_variable_tag(pair: Pair) -> TeraResult { } Rule::logic_expr => expr = Some(parse_logic_expr(p)?), Rule::array_filter => expr = Some(parse_array_with_filters(p)?), - _ => unreachable!("unexpected {:?} rule in parse_variable_tag", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_variable_tag", + p.as_rule() + ))) + } } } Ok(Node::VariableBlock(ws, expr.unwrap())) @@ -558,7 +667,7 @@ fn parse_import_macro(pair: Pair) -> Node { Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } - _ => unreachable!(), + _ => {} // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros }; } @@ -578,7 +687,7 @@ fn parse_extends(pair: Pair) -> Node { Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } - _ => unreachable!(), + _ => {} // Don't call unreachable!, instead return default, this prevents any potential panic due to abuse of macros }; } @@ -603,7 +712,7 @@ fn parse_include(pair: Pair) -> Node { Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } - _ => unreachable!(), + _ => {} // Don't call unreachable!, instead return default, this prevents any potential panic due to abuse of macros }; } @@ -626,7 +735,12 @@ fn parse_set_tag(pair: Pair, global: bool) -> TeraResult { Rule::ident => key = Some(p.as_str().to_string()), Rule::logic_expr => expr = Some(parse_logic_expr(p)?), Rule::array_filter => expr = Some(parse_array_with_filters(p)?), - _ => unreachable!("unexpected {:?} rule in parse_set_tag", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_set_tag", + p.as_rule() + ))) + } } } @@ -645,7 +759,7 @@ fn parse_raw_tag(pair: Pair) -> Node { match p2.as_rule() { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", - _ => unreachable!(), + _ => continue, // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros } } } @@ -655,11 +769,11 @@ fn parse_raw_tag(pair: Pair) -> Node { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", - _ => unreachable!(), + _ => continue, // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros } } } - _ => unreachable!("unexpected {:?} rule in parse_raw_tag", p.as_rule()), + _ => continue, // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros }; } @@ -686,7 +800,12 @@ fn parse_filter_section(pair: Pair) -> TeraResult { args: HashMap::new(), }); } - _ => unreachable!("Got {:?} while parsing filter_tag", p2), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_filter_section", + p2.as_rule() + ))) + } } } } @@ -702,11 +821,21 @@ fn parse_filter_section(pair: Pair) -> TeraResult { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_filter_section", + p2.as_rule() + ))) + } } } } - _ => unreachable!("unexpected {:?} rule in parse_filter_section", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_filter_section", + p.as_rule() + ))) + } }; } Ok(Node::FilterSection(start_ws, FilterSection { filter: filter.unwrap(), body }, end_ws)) @@ -726,7 +855,12 @@ fn parse_block(pair: Pair) -> TeraResult { Rule::tag_start => start_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => start_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => name = Some(p2.as_span().as_str().to_string()), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_block", + p2.as_rule() + ))) + } }; } } @@ -737,11 +871,21 @@ fn parse_block(pair: Pair) -> TeraResult { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => (), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_block", + p2.as_rule() + ))) + } }; } } - _ => unreachable!("unexpected {:?} rule in parse_filter_section", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_block", + p.as_rule() + ))) + } }; } @@ -765,10 +909,20 @@ fn parse_macro_arg(p: Pair) -> TeraResult { "True" => Some(ExprVal::Bool(true)), "false" => Some(ExprVal::Bool(false)), "False" => Some(ExprVal::Bool(false)), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unexpected boolean value: {}", + p.as_str() + ))) + } }, Rule::string => Some(ExprVal::String(replace_string_markers(p.as_str()))), - _ => unreachable!("Got {:?} in parse_macro_arg: {}", p.as_rule(), p.as_str()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_macro_arg", + p.as_rule() + ))) + } }; Ok(val.unwrap()) @@ -829,11 +983,21 @@ fn parse_macro_definition(pair: Pair) -> TeraResult { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => (), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_macro_definition [match endmacro_tag]", + p2.as_rule() + ))) + } }; } } - _ => unreachable!("unexpected {:?} rule in parse_macro_definition", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_macro_definition", + p.as_rule() + ))) + } } } @@ -863,7 +1027,12 @@ fn parse_forloop(pair: Pair) -> TeraResult { container = Some(parse_basic_expr_with_filters(p2)?); } Rule::array_filter => container = Some(parse_array_with_filters(p2)?), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_forloop [match for_tag]", + p2.as_rule() + ))) + } }; } @@ -893,11 +1062,21 @@ fn parse_forloop(pair: Pair) -> TeraResult { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", Rule::ident => (), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_forloop [match endfor_tag]", + p2.as_rule() + ))) + } }; } } - _ => unreachable!("unexpected {:?} rule in parse_forloop", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_forloop", + p.as_rule() + ))) + } }; } @@ -919,7 +1098,7 @@ fn parse_break_tag(pair: Pair) -> Node { Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } - _ => unreachable!(), + _ => continue, // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros }; } @@ -937,7 +1116,7 @@ fn parse_continue_tag(pair: Pair) -> Node { Rule::tag_end => { ws.right = p.as_span().as_str() == "-%}"; } - _ => unreachable!(), + _ => continue, // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros }; } @@ -959,7 +1138,7 @@ fn parse_comment_tag(pair: Pair) -> Node { Rule::comment_text => { content = p.as_str().to_owned(); } - _ => unreachable!(), + _ => continue, // Don't call unreachable!, instead do nothing, this prevents any potential panic due to abuse of macros }; } @@ -994,7 +1173,12 @@ fn parse_if(pair: Pair) -> TeraResult { Rule::tag_start => current_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => current_ws.right = p2.as_span().as_str() == "-%}", Rule::logic_expr => expr = Some(parse_logic_expr(p2)?), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_if [match if_tag/elif_tag]", + p2.as_rule() + ))) + } }; } } @@ -1016,7 +1200,12 @@ fn parse_if(pair: Pair) -> TeraResult { match p2.as_rule() { Rule::tag_start => current_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => current_ws.right = p2.as_span().as_str() == "-%}", - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_if [match else_tag]", + p2.as_rule() + ))) + } }; } } @@ -1032,12 +1221,22 @@ fn parse_if(pair: Pair) -> TeraResult { match p2.as_rule() { Rule::tag_start => end_ws.left = p2.as_span().as_str() == "{%-", Rule::tag_end => end_ws.right = p2.as_span().as_str() == "-%}", - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_if [match endif_tag]", + p2.as_rule() + ))) + } }; } break; } - _ => unreachable!("unreachable rule in parse_if: {:?}", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_if", + p.as_rule() + ))) + } } } @@ -1068,7 +1267,12 @@ fn parse_content(pair: Pair) -> TeraResult> { Rule::filter_section => nodes.push(parse_filter_section(p)?), Rule::text => nodes.push(Node::Text(p.as_span().as_str().to_string())), Rule::block => nodes.push(parse_block(p)?), - _ => unreachable!("unreachable content rule: {:?}", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_content", + p.as_rule() + ))) + } }; } @@ -1218,7 +1422,12 @@ pub fn parse(input: &str) -> TeraResult> { Rule::macro_definition => nodes.push(parse_macro_definition(p)?), Rule::comment_tag => (), Rule::EOI => (), - _ => unreachable!("unknown tpl rule: {:?}", p.as_rule()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse", + p.as_rule() + ))) + } } } diff --git a/src/parser/whitespace.rs b/src/parser/whitespace.rs index 1c7c54c4..5af5c945 100644 --- a/src/parser/whitespace.rs +++ b/src/parser/whitespace.rs @@ -110,7 +110,7 @@ pub fn remove_whitespace(nodes: Vec, body_ws: Option) -> Vec { block.body = remove_whitespace(block.body, Some(body_ws)); res.push(Node::Block(start_ws, block, end_ws)); } - _ => unreachable!(), + _ => {} // Do nothing for unsupported }; continue; } diff --git a/src/renderer/for_loop.rs b/src/renderer/for_loop.rs index 30af339b..6c1981b4 100644 --- a/src/renderer/for_loop.rs +++ b/src/renderer/for_loop.rs @@ -40,7 +40,7 @@ impl<'a> ForLoopValues<'a> { pub fn current_key(&self, i: usize) -> String { match *self { ForLoopValues::Array(_) | ForLoopValues::String(_) => { - unreachable!("No key in array list or string") + i.to_string() // Use the index as the key } ForLoopValues::Object(ref values) => { values.get(i).expect("Failed getting current key").0.clone() diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index aea5cae2..ee3ed4e2 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -375,7 +375,7 @@ impl<'a> Processor<'a> { fn_call.name ))), }, - _ => unreachable!(), + _ => return Err(Error::msg(format!("Unimplemented expression found in line {:?} [{:?}]", s, expr.val))), }; } @@ -504,7 +504,7 @@ impl<'a> Processor<'a> { ); } - Ok(Cow::Owned(tera_fn.call(&args).map_err(err_wrap)?)) + Ok(Cow::Owned(tera_fn.call(self.tera, &args).map_err(err_wrap)?)) } fn eval_macro_call(&mut self, macro_call: &'a MacroCall, write: &mut impl Write) -> Result<()> { @@ -599,7 +599,12 @@ impl<'a> Processor<'a> { LogicOperator::Gt => ll.as_f64().unwrap() > rr.as_f64().unwrap(), LogicOperator::Lte => ll.as_f64().unwrap() <= rr.as_f64().unwrap(), LogicOperator::Lt => ll.as_f64().unwrap() < rr.as_f64().unwrap(), - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "Unimplemented operator for eval_as_bool: {:?} [Gte/Gt/Lte/Lt only]", + operator + ))) + } } } LogicOperator::Eq | LogicOperator::NotEq => { @@ -624,7 +629,12 @@ impl<'a> Processor<'a> { match *operator { LogicOperator::Eq => *lhs_val == *rhs_val, LogicOperator::NotEq => *lhs_val != *rhs_val, - _ => unreachable!(), + _ => { + return Err(Error::msg(format!( + "Unimplemented operator for eval_as_bool: {:?} [Eq/NotEq only]", + operator + ))) + } } } } @@ -670,7 +680,12 @@ impl<'a> Processor<'a> { self.eval_macro_call(macro_call, &mut buf)?; !buf.is_empty() } - _ => unreachable!("unimplemented logic operation for {:?}", bool_expr), + _ => { + return Err(Error::msg(format!( + "Unimplemented logic operation for {:?}", + bool_expr + ))) + } }; if bool_expr.negated { @@ -894,7 +909,7 @@ impl<'a> Processor<'a> { ExprVal::Test(ref test) => { return Err(Error::msg(format!("Tried to do math with a test: {}", test.name))); } - _ => unreachable!("unimplemented math expression for {:?}", expr), + _ => return Err(Error::msg(format!("unimplemented math expression for {:?}", expr))), }; Ok(result) diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs index ca621be7..9d4b1b5d 100644 --- a/src/renderer/tests/basic.rs +++ b/src/renderer/tests/basic.rs @@ -885,7 +885,7 @@ fn can_use_concat_to_push_to_array() { struct Next(AtomicUsize); impl Function for Next { - fn call(&self, _args: &HashMap) -> Result { + fn call(&self, _tera: &Tera, _args: &HashMap) -> Result { Ok(Value::Number(self.0.fetch_add(1, Ordering::Relaxed).into())) } } @@ -894,8 +894,8 @@ impl Function for Next { struct SharedNext(Arc); impl Function for SharedNext { - fn call(&self, args: &HashMap) -> Result { - self.0.call(args) + fn call(&self, tera: &Tera, args: &HashMap) -> Result { + self.0.call(tera, args) } } @@ -974,7 +974,7 @@ fn safe_filter_works() { fn safe_function_works() { struct Safe; impl crate::Function for Safe { - fn call(&self, _args: &HashMap) -> Result { + fn call(&self, _tera: &Tera, _args: &HashMap) -> Result { Ok(Value::String("
Hello
".to_owned())) } From aa16d28ea0b238e76a39a49e76521d5020c180e0 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Thu, 11 Jul 2024 05:04:02 +0000 Subject: [PATCH 05/15] revert change to tera::Function --- src/builtins/functions.rs | 4 ++-- src/renderer/processor.rs | 2 +- src/renderer/tests/basic.rs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/builtins/functions.rs b/src/builtins/functions.rs index ea9c79ca..2fa57d50 100644 --- a/src/builtins/functions.rs +++ b/src/builtins/functions.rs @@ -12,7 +12,7 @@ use crate::errors::{Error, Result}; /// The global function type definition pub trait Function: Sync + Send { /// The global function type definition - fn call(&self, tera: &Tera, args: &HashMap) -> Result; + fn call(&self, args: &HashMap) -> Result; /// Whether the current function's output should be treated as safe, defaults to `false` fn is_safe(&self) -> bool { @@ -24,7 +24,7 @@ impl Function for F where F: Fn(&HashMap) -> Result + Sync + Send, { - fn call(&self, _tera: &Tera, args: &HashMap) -> Result { + fn call(&self, args: &HashMap) -> Result { self(args) } } diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index ee3ed4e2..16d450bf 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -504,7 +504,7 @@ impl<'a> Processor<'a> { ); } - Ok(Cow::Owned(tera_fn.call(self.tera, &args).map_err(err_wrap)?)) + Ok(Cow::Owned(tera_fn.call(&args).map_err(err_wrap)?)) } fn eval_macro_call(&mut self, macro_call: &'a MacroCall, write: &mut impl Write) -> Result<()> { diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs index 9d4b1b5d..ca621be7 100644 --- a/src/renderer/tests/basic.rs +++ b/src/renderer/tests/basic.rs @@ -885,7 +885,7 @@ fn can_use_concat_to_push_to_array() { struct Next(AtomicUsize); impl Function for Next { - fn call(&self, _tera: &Tera, _args: &HashMap) -> Result { + fn call(&self, _args: &HashMap) -> Result { Ok(Value::Number(self.0.fetch_add(1, Ordering::Relaxed).into())) } } @@ -894,8 +894,8 @@ impl Function for Next { struct SharedNext(Arc); impl Function for SharedNext { - fn call(&self, tera: &Tera, args: &HashMap) -> Result { - self.0.call(tera, args) + fn call(&self, args: &HashMap) -> Result { + self.0.call(args) } } @@ -974,7 +974,7 @@ fn safe_filter_works() { fn safe_function_works() { struct Safe; impl crate::Function for Safe { - fn call(&self, _tera: &Tera, _args: &HashMap) -> Result { + fn call(&self, _args: &HashMap) -> Result { Ok(Value::String("
Hello
".to_owned())) } From 18df908ac1ccac5f2ad2a8771cd5bcc7aa1ce343 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Thu, 11 Jul 2024 09:39:21 +0000 Subject: [PATCH 06/15] fix: make context not panic on insert but rather return an error --- benches/big_context.rs | 10 +- benches/templates.rs | 7 +- benches/tera.rs | 30 +++--- examples/basic/main.rs | 8 +- examples/bench/main.rs | 2 +- src/builtins/functions.rs | 1 - src/context.rs | 34 ++++--- src/filter_utils.rs | 1 + src/renderer/call_stack.rs | 14 ++- src/renderer/processor.rs | 2 +- src/renderer/tests/basic.rs | 134 +++++++++++++------------- src/renderer/tests/errors.rs | 14 +-- src/renderer/tests/macros.rs | 6 +- src/renderer/tests/square_brackets.rs | 26 ++--- src/renderer/tests/whitespace.rs | 12 +-- src/tera.rs | 8 +- tests/render_fails.rs | 12 +-- 17 files changed, 166 insertions(+), 155 deletions(-) diff --git a/benches/big_context.rs b/benches/big_context.rs index bca76b94..2b0aa3cd 100644 --- a/benches/big_context.rs +++ b/benches/big_context.rs @@ -71,7 +71,7 @@ fn bench_big_loop_big_object(b: &mut test::Bencher) { )]) .unwrap(); let mut context = Context::new(); - context.insert("objects", &objects); + context.insert("objects", &objects).unwrap(); let rendering = tera.render("big_loop.html", &context).expect("Good render"); assert_eq!(&rendering[..], "0123"); // cloning as making the context is the bottleneck part @@ -93,8 +93,8 @@ fn bench_macro_big_object(b: &mut test::Bencher) { ]) .unwrap(); let mut context = Context::new(); - context.insert("big_object", &big_object); - context.insert("iterations", &(0..500).collect::>()); + context.insert("big_object", &big_object).unwrap(); + context.insert("iterations", &(0..500).collect::>()).unwrap(); let rendering = tera.render("big_loop.html", &context).expect("Good render"); assert_eq!(rendering.len(), 500); assert_eq!(rendering.chars().next().expect("Char"), '1'); @@ -116,7 +116,7 @@ fn bench_macro_big_object_no_loop_with_set(b: &mut test::Bencher) { )]) .unwrap(); let mut context = Context::new(); - context.insert("two_fields", &TwoFields::new()); + context.insert("two_fields", &TwoFields::new()).unwrap(); let rendering = tera.render("no_loop.html", &context).expect("Good render"); assert_eq!(&rendering[..], "\nA\nB\nC\n"); // cloning as making the context is the bottleneck part @@ -143,7 +143,7 @@ fn bench_macro_big_object_no_loop_macro_call(b: &mut test::Bencher) { ]) .unwrap(); let mut context = Context::new(); - context.insert("two_fields", &TwoFields::new()); + context.insert("two_fields", &TwoFields::new()).unwrap(); let rendering = tera.render("no_loop.html", &context).expect("Good render"); assert_eq!(&rendering[..], "A"); // cloning as making the context is the bottleneck part diff --git a/benches/templates.rs b/benches/templates.rs index 5acfaed5..a2a82130 100644 --- a/benches/templates.rs +++ b/benches/templates.rs @@ -24,7 +24,7 @@ pub fn big_table(b: &mut test::Bencher) { let mut tera = Tera::default(); tera.add_raw_templates(vec![("big-table.html", BIG_TABLE_TEMPLATE)]).unwrap(); let mut ctx = Context::new(); - ctx.insert("table", &table); + ctx.insert("table", &table).unwrap(); b.iter(|| tera.render("big-table.html", &ctx)); } @@ -46,7 +46,7 @@ pub fn teams(b: &mut test::Bencher) { let mut tera = Tera::default(); tera.add_raw_templates(vec![("teams.html", TEAMS_TEMPLATE)]).unwrap(); let mut ctx = Context::new(); - ctx.insert("year", &2015); + ctx.insert("year", &2015).unwrap(); ctx.insert( "teams", &vec![ @@ -55,7 +55,8 @@ pub fn teams(b: &mut test::Bencher) { Team { name: "Guangzhou".into(), score: 22 }, Team { name: "Shandong".into(), score: 12 }, ], - ); + ) + .unwrap(); b.iter(|| tera.render("teams.html", &ctx)); } diff --git a/benches/tera.rs b/benches/tera.rs index dc3de3fa..f7bce9bf 100644 --- a/benches/tera.rs +++ b/benches/tera.rs @@ -105,8 +105,8 @@ fn bench_rendering_only_variable(b: &mut test::Bencher) { let mut tera = Tera::default(); tera.add_raw_template("test.html", VARIABLE_ONLY).unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); b.iter(|| tera.render("test.html", &context)); } @@ -116,8 +116,8 @@ fn bench_rendering_basic_template(b: &mut test::Bencher) { let mut tera = Tera::default(); tera.add_raw_template("bench.html", SIMPLE_TEMPLATE).unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); b.iter(|| tera.render("bench.html", &context)); } @@ -127,8 +127,8 @@ fn bench_rendering_only_parent(b: &mut test::Bencher) { let mut tera = Tera::default(); tera.add_raw_templates(vec![("parent.html", PARENT_TEMPLATE)]).unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); b.iter(|| tera.render("parent.html", &context)); } @@ -139,8 +139,8 @@ fn bench_rendering_only_macro_call(b: &mut test::Bencher) { tera.add_raw_templates(vec![("hey.html", USE_MACRO_TEMPLATE), ("macros.html", MACRO_TEMPLATE)]) .unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); b.iter(|| tera.render("hey.html", &context)); } @@ -151,8 +151,8 @@ fn bench_rendering_only_inheritance(b: &mut test::Bencher) { tera.add_raw_templates(vec![("parent.html", PARENT_TEMPLATE), ("child.html", CHILD_TEMPLATE)]) .unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); b.iter(|| tera.render("child.html", &context)); } @@ -167,8 +167,8 @@ fn bench_rendering_inheritance_and_macros(b: &mut test::Bencher) { ]) .unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); b.iter(|| tera.render("child.html", &context)); } @@ -209,7 +209,7 @@ fn bench_huge_loop(b: &mut test::Bencher) { )]) .unwrap(); let mut context = Context::new(); - context.insert("rows", &rows); + context.insert("rows", &rows).unwrap(); b.iter(|| tera.render("huge.html", &context.clone())); } @@ -256,7 +256,7 @@ fn access_deep_object(b: &mut test::Bencher) { .unwrap(); let mut context = Context::new(); println!("{:?}", deep_object()); - context.insert("deep_object", &deep_object()); + context.insert("deep_object", &deep_object()).unwrap(); assert!(tera.render("deep_object.html", &context).unwrap().contains("ornery")); b.iter(|| tera.render("deep_object.html", &context)); @@ -274,7 +274,7 @@ fn access_deep_object_with_literal(b: &mut test::Bencher) { )]) .unwrap(); let mut context = Context::new(); - context.insert("deep_object", &deep_object()); + context.insert("deep_object", &deep_object()).unwrap(); assert!(tera.render("deep_object.html", &context).unwrap().contains("ornery")); b.iter(|| tera.render("deep_object.html", &context)); diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 77652670..95dd5f79 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -32,10 +32,10 @@ pub fn do_nothing_filter(value: &Value, _: &HashMap) -> Resultalert('pwnd');"); + context.insert("username", &"Bob").unwrap(); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); + context.insert("show_all", &false).unwrap(); + context.insert("bio", &"").unwrap(); // A one off template Tera::one_off("hello", &Context::new(), true).unwrap(); diff --git a/examples/bench/main.rs b/examples/bench/main.rs index 0c42e55a..823d8d33 100644 --- a/examples/bench/main.rs +++ b/examples/bench/main.rs @@ -27,7 +27,7 @@ fn main() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("big-table.html", BIG_TABLE_TEMPLATE)]).unwrap(); let mut ctx = Context::new(); - ctx.insert("table", &table); + ctx.insert("table", &table).unwrap(); let _ = tera.render("big-table.html", &ctx).unwrap(); println!("Done!"); diff --git a/src/builtins/functions.rs b/src/builtins/functions.rs index 2fa57d50..a2690e29 100644 --- a/src/builtins/functions.rs +++ b/src/builtins/functions.rs @@ -1,4 +1,3 @@ -use crate::Tera; use std::collections::HashMap; #[cfg(feature = "builtins")] diff --git a/src/context.rs b/src/context.rs index a343338b..0dc33ba4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -23,15 +23,19 @@ impl Context { /// Converts the `val` parameter to `Value` and insert it into the context. /// - /// Panics if the serialization fails. - /// /// ```rust /// # use tera::Context; /// let mut context = tera::Context::new(); /// context.insert("number_users", &42); /// ``` - pub fn insert>(&mut self, key: S, val: &T) { - self.data.insert(key.into(), to_value(val).unwrap()); + pub fn insert>( + &mut self, + key: S, + val: &T, + ) -> Result<(), Error> { + self.data.insert(key.into(), to_value(val)?); + + Ok(()) } /// Converts the `val` parameter to `Value` and insert it into the context. @@ -458,11 +462,11 @@ mod tests { #[test] fn can_extend_context() { let mut target = Context::new(); - target.insert("a", &1); - target.insert("b", &2); + target.insert("a", &1).unwrap(); + target.insert("b", &2).unwrap(); let mut source = Context::new(); - source.insert("b", &3); - source.insert("c", &4); + source.insert("b", &3).unwrap(); + source.insert("c", &4).unwrap(); target.extend(source); assert_eq!(*target.data.get("a").unwrap(), to_value(1).unwrap()); assert_eq!(*target.data.get("b").unwrap(), to_value(3).unwrap()); @@ -477,8 +481,8 @@ mod tests { }); let context_from_value = Context::from_value(obj).unwrap(); let mut context = Context::new(); - context.insert("name", "bob"); - context.insert("age", &25); + context.insert("name", "bob").unwrap(); + context.insert("age", &25).unwrap(); assert_eq!(context_from_value, context); } @@ -489,19 +493,19 @@ mod tests { map.insert("last_name", "something"); let context_from_serialize = Context::from_serialize(&map).unwrap(); let mut context = Context::new(); - context.insert("name", "bob"); - context.insert("last_name", "something"); + context.insert("name", "bob").unwrap(); + context.insert("last_name", "something").unwrap(); assert_eq!(context_from_serialize, context); } #[test] fn can_remove_a_key() { let mut context = Context::new(); - context.insert("name", "foo"); - context.insert("bio", "Hi, I'm foo."); + context.insert("name", "foo").unwrap(); + context.insert("bio", "Hi, I'm foo.").unwrap(); let mut expected = Context::new(); - expected.insert("name", "foo"); + expected.insert("name", "foo").unwrap(); assert_eq!(context.remove("bio"), Some(to_value("Hi, I'm foo.").unwrap())); assert_eq!(context.get("bio"), None); assert_eq!(context, expected); diff --git a/src/filter_utils.rs b/src/filter_utils.rs index 9079ccd8..fae0e145 100644 --- a/src/filter_utils.rs +++ b/src/filter_utils.rs @@ -20,6 +20,7 @@ impl Ord for OrderedF64 { } } +#[allow(clippy::non_canonical_partial_ord_impl)] impl PartialOrd for OrderedF64 { fn partial_cmp(&self, other: &OrderedF64) -> Option { Some(total_cmp(&self.0, &other.0)) diff --git a/src/renderer/call_stack.rs b/src/renderer/call_stack.rs index 2ff364d2..90ff4ea6 100644 --- a/src/renderer/call_stack.rs +++ b/src/renderer/call_stack.rs @@ -194,7 +194,7 @@ impl<'a> CallStack<'a> { self.current_frame().active_template } - pub fn current_context_cloned(&self) -> Value { + pub fn current_context_cloned(&self) -> Result { let mut context = HashMap::new(); // Go back the stack in reverse to see what we have access to @@ -207,14 +207,17 @@ impl<'a> CallStack<'a> { ); if for_loop.is_key_value() { context.insert( - for_loop.key_name.clone().unwrap(), + for_loop + .key_name + .clone() + .ok_or_else(|| Error::msg("Expected key name in key-value for loop"))?, Value::String(for_loop.get_current_key()), ); } } // Macros don't have access to the user context, we're done if frame.kind == FrameType::Macro { - return to_value(&context).unwrap(); + return Ok(to_value(&context)?); } } @@ -223,8 +226,9 @@ impl<'a> CallStack<'a> { // We do it this way as we can override global variable temporarily in forloops let mut new_ctx = self.context.inner.clone(); for (key, val) in context { - new_ctx.insert(key, &val) + new_ctx.insert(key, &val)? } - new_ctx.into_json() + + Ok(new_ctx.into_json()) } } diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index 16d450bf..a80d02c9 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -957,7 +957,7 @@ impl<'a> Processor<'a> { // Unwraps are safe since we are dealing with things that are already Value return Ok(Cow::Owned( to_value( - to_string_pretty(&self.call_stack.current_context_cloned().take()).unwrap(), + to_string_pretty(&self.call_stack.current_context_cloned()?.take()).unwrap(), ) .unwrap(), )); diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs index ca621be7..f2cdf8bb 100644 --- a/src/renderer/tests/basic.rs +++ b/src/renderer/tests/basic.rs @@ -72,14 +72,14 @@ fn render_variable_block_lit_expr() { #[test] fn render_variable_block_ident() { let mut context = Context::new(); - context.insert("name", &"john"); - context.insert("malicious", &""); - context.insert("a", &2); - context.insert("b", &3); - context.insert("numbers", &vec![1, 2, 3]); - context.insert("tuple_list", &vec![(1, 2, 3), (1, 2, 3)]); - context.insert("review", &Review::new()); - context.insert("with_newline", &"Animal Alphabets\nB is for Bee-Eater"); + context.insert("name", &"john").unwrap(); + context.insert("malicious", &"").unwrap(); + context.insert("a", &2).unwrap(); + context.insert("b", &3).unwrap(); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); + context.insert("tuple_list", &vec![(1, 2, 3), (1, 2, 3)]).unwrap(); + context.insert("review", &Review::new()).unwrap(); + context.insert("with_newline", &"Animal Alphabets\nB is for Bee-Eater").unwrap(); let inputs = vec![ ("{{ name }}", "john"), @@ -144,18 +144,18 @@ fn render_variable_block_ident() { #[test] fn render_variable_block_logic_expr() { let mut context = Context::new(); - context.insert("name", &"john"); - context.insert("malicious", &""); - context.insert("a", &2); - context.insert("b", &3); - context.insert("numbers", &vec![1, 2, 3]); - context.insert("tuple_list", &vec![(1, 2, 3), (1, 2, 3)]); + context.insert("name", &"john").unwrap(); + context.insert("malicious", &"").unwrap(); + context.insert("a", &2).unwrap(); + context.insert("b", &3).unwrap(); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); + context.insert("tuple_list", &vec![(1, 2, 3), (1, 2, 3)]).unwrap(); let mut hashmap = HashMap::new(); hashmap.insert("a", 1); hashmap.insert("b", 10); hashmap.insert("john", 100); - context.insert("object", &hashmap); - context.insert("urls", &vec!["https://test"]); + context.insert("object", &hashmap).unwrap(); + context.insert("urls", &vec!["https://test"]).unwrap(); let inputs = vec![ ("{{ (1.9 + a) | round > 10 }}", "false"), @@ -199,8 +199,8 @@ fn render_variable_block_logic_expr() { #[test] fn render_variable_block_autoescaping_disabled() { let mut context = Context::new(); - context.insert("name", &"john"); - context.insert("malicious", &""); + context.insert("name", &"john").unwrap(); + context.insert("malicious", &"").unwrap(); let inputs = vec![ ("{{ name }}", "john"), @@ -245,7 +245,7 @@ fn escaping_happens_at_the_end() { for (input, expected) in inputs { let mut context = Context::new(); - context.insert("url", "https://www.example.org/apples-&-oranges/"); + context.insert("url", "https://www.example.org/apples-&-oranges/").unwrap(); assert_eq!(render_template(input, &context).unwrap(), expected); } } @@ -253,8 +253,8 @@ fn escaping_happens_at_the_end() { #[test] fn filter_args_are_not_escaped() { let mut context = Context::new(); - context.insert("my_var", &"hey"); - context.insert("to", &"&"); + context.insert("my_var", &"hey").unwrap(); + context.insert("to", &"&").unwrap(); let input = r#"{{ my_var | replace(from="h", to=to) }}"#; assert_eq!(render_template(input, &context).unwrap(), "&ey"); @@ -331,10 +331,10 @@ fn render_raw_tag() { #[test] fn add_set_values_in_context() { let mut context = Context::new(); - context.insert("my_var", &"hey"); - context.insert("malicious", &""); - context.insert("admin", &true); - context.insert("num", &1); + context.insert("my_var", &"hey").unwrap(); + context.insert("malicious", &"").unwrap(); + context.insert("admin", &true).unwrap(); + context.insert("num", &1).unwrap(); let inputs = vec![ ("{% set i = 1 %}{{ i }}", "1"), @@ -383,15 +383,15 @@ fn render_filter_section() { #[test] fn render_tests() { let mut context = Context::new(); - context.insert("is_true", &true); - context.insert("is_false", &false); - context.insert("age", &18); - context.insert("name", &"john"); + context.insert("is_true", &true).unwrap(); + context.insert("is_false", &false).unwrap(); + context.insert("age", &18).unwrap(); + context.insert("name", &"john").unwrap(); let mut map = HashMap::new(); map.insert(0, 1); - context.insert("map", &map); - context.insert("numbers", &vec![1, 2, 3]); - context.insert::, _>("maybe", &None); + context.insert("map", &map).unwrap(); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); + context.insert::, _>("maybe", &None).unwrap(); let inputs = vec![ ("{% if is_true is defined %}Admin{% endif %}", "Admin"), @@ -420,12 +420,12 @@ fn render_tests() { #[test] fn render_if_elif_else() { let mut context = Context::new(); - context.insert("is_true", &true); - context.insert("is_false", &false); - context.insert("age", &18); - context.insert("name", &"john"); - context.insert("empty_string", &""); - context.insert("numbers", &vec![1, 2, 3]); + context.insert("is_true", &true).unwrap(); + context.insert("is_false", &false).unwrap(); + context.insert("age", &18).unwrap(); + context.insert("name", &"john").unwrap(); + context.insert("empty_string", &"").unwrap(); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); let inputs = vec![ ("{% if is_true %}Admin{% endif %}", "Admin"), @@ -504,12 +504,12 @@ fn render_for() { map.insert("name", "bob"); map.insert("age", "18"); - context.insert("data", &vec![1, 2, 3]); - context.insert("notes", &vec![1, 2, 3]); - context.insert("vectors", &vec![vec![0, 3, 6], vec![1, 4, 7]]); - context.insert("vectors_some_empty", &vec![vec![0, 3, 6], vec![], vec![1, 4, 7]]); - context.insert("map", &map); - context.insert("truthy", &2); + context.insert("data", &vec![1, 2, 3]).unwrap(); + context.insert("notes", &vec![1, 2, 3]).unwrap(); + context.insert("vectors", &vec![vec![0, 3, 6], vec![1, 4, 7]]).unwrap(); + context.insert("vectors_some_empty", &vec![vec![0, 3, 6], vec![], vec![1, 4, 7]]).unwrap(); + context.insert("map", &map).unwrap(); + context.insert("truthy", &2).unwrap(); let inputs = vec![ ("{% for i in data %}{{i}}{% endfor %}", "123"), @@ -591,7 +591,7 @@ fn render_for() { #[test] fn render_magic_variable_isnt_escaped() { let mut context = Context::new(); - context.insert("html", &""); + context.insert("html", &"").unwrap(); let result = render_template("{{ __tera_context }}", &context); @@ -608,7 +608,7 @@ fn render_magic_variable_isnt_escaped() { #[test] fn ok_many_variable_blocks() { let mut context = Context::new(); - context.insert("username", &"bob"); + context.insert("username", &"bob").unwrap(); let mut tpl = String::new(); for _ in 0..200 { @@ -624,8 +624,8 @@ fn ok_many_variable_blocks() { #[test] fn can_set_variable_in_global_context_in_forloop() { let mut context = Context::new(); - context.insert("tags", &vec![1, 2, 3]); - context.insert("default", &"default"); + context.insert("tags", &vec![1, 2, 3]).unwrap(); + context.insert("default", &"default").unwrap(); let result = render_template( r#" @@ -644,8 +644,8 @@ fn can_set_variable_in_global_context_in_forloop() { fn default_filter_works() { let mut context = Context::new(); let i: Option = None; - context.insert("existing", "hello"); - context.insert("null", &i); + context.insert("existing", "hello").unwrap(); + context.insert("null", &i).unwrap(); let inputs = vec![ (r#"{{ existing | default(value="hey") }}"#, "hello"), @@ -673,7 +673,7 @@ fn filter_filter_works() { } let mut context = Context::new(); - context.insert("authors", &vec![Author { id: 1 }, Author { id: 2 }, Author { id: 3 }]); + context.insert("authors", &vec![Author { id: 1 }, Author { id: 2 }, Author { id: 3 }]).unwrap(); let inputs = vec![(r#"{{ authors | filter(attribute="id", value=1) | first | get(key="id") }}"#, "1")]; @@ -688,8 +688,8 @@ fn filter_filter_works() { fn filter_on_array_literal_works() { let mut context = Context::new(); let i: Option = None; - context.insert("existing", "hello"); - context.insert("null", &i); + context.insert("existing", "hello").unwrap(); + context.insert("null", &i).unwrap(); let inputs = vec![ (r#"{{ [1, 2, 3] | length }}"#, "3"), @@ -706,10 +706,10 @@ fn filter_on_array_literal_works() { #[test] fn can_do_string_concat() { let mut context = Context::new(); - context.insert("a_string", "hello"); - context.insert("another_string", "xXx"); - context.insert("an_int", &1); - context.insert("a_float", &3.18); + context.insert("a_string", "hello").unwrap(); + context.insert("another_string", "xXx").unwrap(); + context.insert("an_int", &1).unwrap(); + context.insert("a_float", &3.18).unwrap(); let inputs = vec![ (r#"{{ "hello" ~ " world" }}"#, "hello world"), @@ -735,7 +735,7 @@ fn can_do_string_concat() { #[test] fn can_fail_rendering_from_template() { let mut context = Context::new(); - context.insert("title", "hello"); + context.insert("title", "hello").unwrap(); let res = render_template( r#"{{ throw(message="Error: " ~ title ~ " did not include a summary") }}"#, @@ -764,7 +764,7 @@ fn does_render_owned_for_loop_with_objects() { {"id": 8}, {"id": 9, "year": null}, ]); - context.insert("something", &data); + context.insert("something", &data).unwrap(); let tpl = r#"{% for year, things in something | group_by(attribute="year") %}{{year}},{% endfor %}"#; @@ -786,7 +786,7 @@ fn does_render_owned_for_loop_with_objects_string_keys() { {"id": 8}, {"id": 9, "year": null}, ]); - context.insert("something", &data); + context.insert("something", &data).unwrap(); let tpl = r#"{% for group, things in something | group_by(attribute="group") %}{{group}},{% endfor %}"#; let expected = "a,b,c,"; @@ -796,9 +796,9 @@ fn does_render_owned_for_loop_with_objects_string_keys() { #[test] fn render_magic_variable_gets_all_contexts() { let mut context = Context::new(); - context.insert("html", &""); - context.insert("num", &1); - context.insert("i", &10); + context.insert("html", &"").unwrap(); + context.insert("num", &1).unwrap(); + context.insert("i", &10).unwrap(); let result = render_template( "{% set some_val = 1 %}{% for i in range(start=0, end=1) %}{% set for_val = i %}{{ __tera_context }}{% endfor %}", @@ -821,9 +821,9 @@ fn render_magic_variable_gets_all_contexts() { #[test] fn render_magic_variable_macro_doesnt_leak() { let mut context = Context::new(); - context.insert("html", &""); - context.insert("num", &1); - context.insert("i", &10); + context.insert("html", &"").unwrap(); + context.insert("num", &1).unwrap(); + context.insert("i", &10).unwrap(); let mut tera = Tera::default(); tera.add_raw_templates(vec![ @@ -934,7 +934,7 @@ fn split_on_context_value() { let mut tera = Tera::default(); tera.add_raw_template("split.html", r#"{{ body | split(pat="\n") }}"#).unwrap(); let mut context = Context::new(); - context.insert("body", "multi\nple\nlines"); + context.insert("body", "multi\nple\nlines").unwrap(); let res = tera.render("split.html", &context); assert_eq!(res.unwrap(), "[multi, ple, lines]"); } diff --git a/src/renderer/tests/errors.rs b/src/renderer/tests/errors.rs index 1a0a6703..f7bfc3fa 100644 --- a/src/renderer/tests/errors.rs +++ b/src/renderer/tests/errors.rs @@ -104,7 +104,7 @@ fn error_out_of_range_index() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ arr[10] }}")]).unwrap(); let mut context = Context::new(); - context.insert("arr", &[1, 2, 3]); + context.insert("arr", &[1, 2, 3]).unwrap(); let result = tera.render("tpl", &Context::new()); @@ -119,7 +119,7 @@ fn error_unknown_index_variable() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ arr[a] }}")]).unwrap(); let mut context = Context::new(); - context.insert("arr", &[1, 2, 3]); + context.insert("arr", &[1, 2, 3]).unwrap(); let result = tera.render("tpl", &context); @@ -135,8 +135,8 @@ fn error_invalid_type_index_variable() { tera.add_raw_templates(vec![("tpl", "{{ arr[a] }}")]).unwrap(); let mut context = Context::new(); - context.insert("arr", &[1, 2, 3]); - context.insert("a", &true); + context.insert("arr", &[1, 2, 3]).unwrap(); + context.insert("a", &true).unwrap(); let result = tera.render("tpl", &context); @@ -168,7 +168,7 @@ fn error_when_using_variable_set_in_included_templates_outside() { ]) .unwrap(); let mut context = Context::new(); - context.insert("a", &10); + context.insert("a", &10).unwrap(); let result = tera.render("base", &context); assert_eq!( @@ -184,7 +184,7 @@ fn right_variable_name_is_needed_in_for_loop() { let mut data = HashMap::new(); data.insert("content", "hello"); let mut context = Context::new(); - context.insert("comments", &vec![data]); + context.insert("comments", &vec![data]).unwrap(); let mut tera = Tera::default(); tera.add_raw_template( "tpl", @@ -229,7 +229,7 @@ fn error_string_concat_math_logic() { let mut tera = Tera::default(); tera.add_raw_templates(vec![("tpl", "{{ 'ho' ~ name < 10 }}")]).unwrap(); let mut context = Context::new(); - context.insert("name", &"john"); + context.insert("name", &"john").unwrap(); let result = tera.render("tpl", &context); diff --git a/src/renderer/tests/macros.rs b/src/renderer/tests/macros.rs index 7a86e667..1eb99c33 100644 --- a/src/renderer/tests/macros.rs +++ b/src/renderer/tests/macros.rs @@ -36,7 +36,7 @@ fn render_macros_defined_in_template() { #[test] fn render_macros_expression_arg() { let mut context = Context::new(); - context.insert("pages", &vec![1, 2, 3, 4, 5]); + context.insert("pages", &vec![1, 2, 3, 4, 5]).unwrap(); let mut tera = Tera::default(); tera.add_raw_templates(vec![ ("macros", "{% macro hello(val)%}{{val}}{% endmacro hello %}"), @@ -104,7 +104,7 @@ fn macro_param_arent_escaped() { ]) .unwrap(); let mut context = Context::new(); - context.insert("my_var", &"&"); + context.insert("my_var", &"&").unwrap(); let result = tera.render("hello.html", &context); assert_eq!(result.unwrap(), "&".to_string()); @@ -177,7 +177,7 @@ fn recursive_macro_with_loops() { numbers: vec![1, 2, 3], }; let mut context = Context::new(); - context.insert("objects", &vec![child]); + context.insert("objects", &vec![child]).unwrap(); let mut tera = Tera::default(); tera.add_raw_templates(vec![ diff --git a/src/renderer/tests/square_brackets.rs b/src/renderer/tests/square_brackets.rs index eb1e6577..8c4cff1e 100644 --- a/src/renderer/tests/square_brackets.rs +++ b/src/renderer/tests/square_brackets.rs @@ -14,12 +14,14 @@ struct Test { #[test] fn var_access_by_square_brackets() { let mut context = Context::new(); - context.insert( - "var", - &Test { a: "hi".into(), b: "i_am_actually_b".into(), c: vec!["fred".into()] }, - ); - context.insert("zero", &0); - context.insert("a", "b"); + context + .insert( + "var", + &Test { a: "hi".into(), b: "i_am_actually_b".into(), c: vec!["fred".into()] }, + ) + .unwrap(); + context.insert("zero", &0).unwrap(); + context.insert("a", "b").unwrap(); let mut map = HashMap::new(); map.insert("true", "yes"); @@ -28,9 +30,9 @@ fn var_access_by_square_brackets() { map.insert("with/slash", "works"); let mut deep_map = HashMap::new(); deep_map.insert("inner_map", &map); - context.insert("map", &map); - context.insert("deep_map", &deep_map); - context.insert("bool_vec", &vec!["true", "false"]); + context.insert("map", &map).unwrap(); + context.insert("deep_map", &deep_map).unwrap(); + context.insert("bool_vec", &vec!["true", "false"]).unwrap(); let inputs = vec![ ("{{var.a}}", "hi"), @@ -54,7 +56,7 @@ fn var_access_by_square_brackets() { #[test] fn var_access_by_square_brackets_errors() { let mut context = Context::new(); - context.insert("var", &Test { a: "hi".into(), b: "there".into(), c: vec![] }); + context.insert("var", &Test { a: "hi".into(), b: "there".into(), c: vec![] }).unwrap(); let t = Tera::one_off("{{var[csd]}}", &context, true); assert!(t.is_err(), "Access of csd should be impossible"); } @@ -98,10 +100,10 @@ fn var_access_by_loop_index_with_set() { #[test] fn can_get_value_if_key_contains_period() { let mut context = Context::new(); - context.insert("name", "Mt. Robson Provincial Park"); + context.insert("name", "Mt. Robson Provincial Park").unwrap(); let mut map = HashMap::new(); map.insert("Mt. Robson Provincial Park".to_string(), "hello".to_string()); - context.insert("tag_info", &map); + context.insert("tag_info", &map).unwrap(); let res = Tera::one_off(r#"{{ tag_info[name] }}"#, &context, true); assert!(res.is_ok()); diff --git a/src/renderer/tests/whitespace.rs b/src/renderer/tests/whitespace.rs index 92ab6244..7f154efc 100644 --- a/src/renderer/tests/whitespace.rs +++ b/src/renderer/tests/whitespace.rs @@ -4,7 +4,7 @@ use crate::tera::Tera; #[test] fn can_remove_whitespace_basic() { let mut context = Context::new(); - context.insert("numbers", &vec![1, 2, 3]); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); let inputs = vec![ (" {%- for n in numbers %}{{n}}{% endfor -%} ", "123"), @@ -39,7 +39,7 @@ fn can_remove_whitespace_basic() { #[test] fn can_remove_whitespace_include() { let mut context = Context::new(); - context.insert("numbers", &vec![1, 2, 3]); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); let inputs = vec![ (r#"Hi {%- include "include" -%} "#, "HiIncluded"), @@ -57,7 +57,7 @@ fn can_remove_whitespace_include() { #[test] fn can_remove_whitespace_macros() { let mut context = Context::new(); - context.insert("numbers", &vec![1, 2, 3]); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); let inputs = vec![ (r#" {%- import "macros" as macros -%} {{macros::hey()}}"#, "Hey!"), @@ -79,7 +79,7 @@ fn can_remove_whitespace_macros() { #[test] fn can_remove_whitespace_inheritance() { let mut context = Context::new(); - context.insert("numbers", &vec![1, 2, 3]); + context.insert("numbers", &vec![1, 2, 3]).unwrap(); let inputs = vec![ (r#"{%- extends "base" -%} {% block content %}{{super()}}{% endblock %}"#, " Hey! "), @@ -102,7 +102,7 @@ fn can_remove_whitespace_inheritance() { #[test] fn works_with_filter_section() { let mut context = Context::new(); - context.insert("d", "d"); + context.insert("d", "d").unwrap(); let input = r#"{% filter upper %} {{ "c" }} d{% endfilter %}"#; let res = Tera::one_off(input, &context, true).unwrap(); assert_eq!(res, " C D"); @@ -111,7 +111,7 @@ fn works_with_filter_section() { #[test] fn make_sure_not_to_delete_whitespaces() { let mut context = Context::new(); - context.insert("d", "d"); + context.insert("d", "d").unwrap(); let input = r#"{% raw %} yaml_test: {% endraw %}"#; let res = Tera::one_off(input, &context, true).unwrap(); assert_eq!(res, " yaml_test: "); diff --git a/src/tera.rs b/src/tera.rs index aab77574..5af80130 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -1047,7 +1047,7 @@ mod tests { #[test] fn test_can_autoescape_one_off_template() { let mut context = Context::new(); - context.insert("greeting", &"

"); + context.insert("greeting", &"

").unwrap(); let result = Tera::one_off("{{ greeting }} world", &context, true).unwrap(); assert_eq!(result, "<p> world"); @@ -1056,7 +1056,7 @@ mod tests { #[test] fn test_can_disable_autoescape_one_off_template() { let mut context = Context::new(); - context.insert("greeting", &"

"); + context.insert("greeting", &"

").unwrap(); let result = Tera::one_off("{{ greeting }} world", &context, false).unwrap(); assert_eq!(result, "

world"); @@ -1084,7 +1084,7 @@ mod tests { tera.autoescape_on(vec!["foo"]); tera.set_escape_fn(escape_c_string); let mut context = Context::new(); - context.insert("content", &"Hello\n\'world\"!"); + context.insert("content", &"Hello\n\'world\"!").unwrap(); let result = tera.render("foo", &context).unwrap(); assert_eq!(result, r#""Hello\n\'world\"!""#); } @@ -1098,7 +1098,7 @@ mod tests { tera.set_escape_fn(no_escape); tera.reset_escape_fn(); let mut context = Context::new(); - context.insert("content", &"Hello\n\'world\"!"); + context.insert("content", &"Hello\n\'world\"!").unwrap(); let result = tera.render("foo", &context).unwrap(); assert_eq!(result, "Hello\n'world"!"); } diff --git a/tests/render_fails.rs b/tests/render_fails.rs index 0fd0e594..b22039a0 100644 --- a/tests/render_fails.rs +++ b/tests/render_fails.rs @@ -11,12 +11,12 @@ use crate::common::{Product, Review}; fn render_tpl(tpl_name: &str) -> Result { let tera = Tera::new("tests/render-failures/**/*").unwrap(); let mut context = Context::new(); - context.insert("product", &Product::new()); - context.insert("username", &"bob"); - context.insert("friend_reviewed", &true); - context.insert("number_reviews", &2); - context.insert("show_more", &true); - context.insert("reviews", &vec![Review::new(), Review::new()]); + context.insert("product", &Product::new()).unwrap(); + context.insert("username", &"bob").unwrap(); + context.insert("friend_reviewed", &true).unwrap(); + context.insert("number_reviews", &2).unwrap(); + context.insert("show_more", &true).unwrap(); + context.insert("reviews", &vec![Review::new(), Review::new()]).unwrap(); tera.render(tpl_name, &context) } From 10823e280ee03455b0388e059424173778a0a4c3 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Thu, 11 Jul 2024 10:57:34 +0000 Subject: [PATCH 07/15] Add __tera_context_raw to render context as the json it is --- src/renderer/processor.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index a80d02c9..36e213ce 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -20,6 +20,9 @@ use crate::Context; /// Special string indicating request to dump context static MAGICAL_DUMP_VAR: &str = "__tera_context"; +/// Special string indicating request to dump context as the object it is +static MAGICAL_DUMP_VAR_RAW: &str = "__tera_context_raw"; + /// This will convert a Tera variable to a json pointer if it is possible by replacing /// the index with their evaluated stringified value fn evaluate_sub_variables(key: &str, call_stack: &CallStack) -> Result { @@ -963,6 +966,10 @@ impl<'a> Processor<'a> { )); } + if key == MAGICAL_DUMP_VAR_RAW { + return Ok(Cow::Owned(self.call_stack.current_context_cloned()?)); + } + process_path(key, &self.call_stack) } From 7ec148c64ad86e4420fe411cf0b7db04d7af4f86 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Fri, 12 Jul 2024 06:52:19 +0000 Subject: [PATCH 08/15] feat: handle more cases of sync code in async --- Cargo.toml | 5 +- src/renderer/mod.rs | 2 +- src/renderer/processor.rs | 267 ++++++++++++++++++++++++++++++++++++-- src/tera.rs | 2 +- 4 files changed, 262 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 843ea556..82614f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,9 @@ chrono-tz = {version = "0.9", optional = true} # used in get_random function rand = {version = "0.8", optional = true} +# used in async +async-recursion = {version = "1", optional = true} + [dev-dependencies] serde_derive = "1.0" pretty_assertions = "1" @@ -45,7 +48,7 @@ tempfile = "3" [features] default = ["builtins", "async"] -async = [] +async = ["dep:async-recursion"] builtins = ["urlencode", "slug", "humansize", "chrono", "chrono-tz", "rand"] urlencode = ["percent-encoding"] preserve_order = ["serde_json/preserve_order"] diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 6b95e5a7..0437cfb6 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -70,7 +70,7 @@ impl<'a> Renderer<'a> { /// Async version of [`render_to()`](Self::render_to) #[cfg(feature = "async")] - pub async fn render_to_async(&self, mut output: impl Write) -> Result<()> { + pub async fn render_to_async(&self, mut output: (impl Write + Send + Sync)) -> Result<()> { let mut processor = Processor::new(self.template, self.tera, self.context, self.should_escape); diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index 36e213ce..d790fa04 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -162,7 +162,27 @@ impl<'a> Processor<'a> { Ok(()) } - fn render_for_loop(&mut self, for_loop: &'a Forloop, write: &mut impl Write) -> Result<()> { + #[cfg(feature = "async")] + #[async_recursion::async_recursion] + async fn render_body_async( + &mut self, + body: &'a [Node], + write: &mut (impl Write + Send + Sync), + ) -> Result<()> { + for n in body { + self.render_node_async(n, write).await?; + + if self.call_stack.should_break_body() { + break; + } + } + + Ok(()) + } + + /// Helper method to create a for loop, this makes async for loop rendering easier + #[inline] + fn create_for_loop(&mut self, for_loop: &'a Forloop) -> Result> { let container_name = match for_loop.container.val { ExprVal::Ident(ref ident) => ident, ExprVal::FunctionCall(FunctionCall { ref name, .. }) => name, @@ -173,10 +193,6 @@ impl<'a> Processor<'a> { ))), }; - let for_loop_name = &for_loop.value; - let for_loop_body = &for_loop.body; - let for_loop_empty_body = &for_loop.empty_body; - let container_val = self.safe_eval_expression(&for_loop.container)?; let for_loop = match *container_val { @@ -224,6 +240,16 @@ impl<'a> Processor<'a> { } }; + Ok(for_loop) + } + + fn render_for_loop(&mut self, for_loop: &'a Forloop, write: &mut impl Write) -> Result<()> { + let for_loop_name = &for_loop.value; + let for_loop_body = &for_loop.body; + let for_loop_empty_body = &for_loop.empty_body; + + let for_loop = self.create_for_loop(for_loop)?; + let len = for_loop.len(); match (len, for_loop_empty_body) { (0, Some(empty_body)) => self.render_body(empty_body, write), @@ -248,6 +274,42 @@ impl<'a> Processor<'a> { } } + #[cfg(feature = "async")] + async fn render_for_loop_async( + &mut self, + for_loop: &'a Forloop, + write: &mut (impl Write + Send + Sync), + ) -> Result<()> { + let for_loop_name = &for_loop.value; + let for_loop_body = &for_loop.body; + let for_loop_empty_body = &for_loop.empty_body; + + let for_loop = self.create_for_loop(for_loop)?; + + let len = for_loop.len(); + match (len, for_loop_empty_body) { + (0, Some(empty_body)) => self.render_body_async(empty_body, write).await, + (0, _) => Ok(()), + (_, _) => { + self.call_stack.push_for_loop_frame(for_loop_name, for_loop); + + for _ in 0..len { + self.render_body_async(for_loop_body, write).await?; + + if self.call_stack.should_break_for_loop() { + break; + } + + self.call_stack.increment_for_loop()?; + } + + self.call_stack.pop(); + + Ok(()) + } + } + } + fn render_if_node(&mut self, if_node: &'a If, write: &mut impl Write) -> Result<()> { for (_, expr, body) in &if_node.conditions { if self.eval_as_bool(expr)? { @@ -262,6 +324,25 @@ impl<'a> Processor<'a> { Ok(()) } + #[cfg(feature = "async")] + async fn render_if_node_async( + &mut self, + if_node: &'a If, + write: &mut (impl Write + Send + Sync), + ) -> Result<()> { + for (_, expr, body) in &if_node.conditions { + if self.eval_as_bool(expr)? { + return self.render_body_async(body, write).await; + } + } + + if let Some((_, ref body)) = if_node.otherwise { + return self.render_body_async(body, write).await; + } + + Ok(()) + } + /// The way inheritance work is that the top parent will be rendered by the renderer so for blocks /// we want to look from the bottom (`level = 0`, the template the user is actually rendering) /// to the top (the base template). @@ -297,6 +378,45 @@ impl<'a> Processor<'a> { self.render_body(&block.body, write) } + #[cfg(feature = "async")] + /// The way inheritance work is that the top parent will be rendered by the renderer so for blocks + /// we want to look from the bottom (`level = 0`, the template the user is actually rendering) + /// to the top (the base template). + /// + /// This is the async version of (`render_block`)[Self::render_block] + #[async_recursion::async_recursion] + async fn render_block_async( + &mut self, + block: &'a Block, + level: usize, + write: &mut (impl Write + Send + Sync), + ) -> Result<()> { + let level_template = match level { + 0 => self.call_stack.active_template(), + _ => self + .tera + .get_template(&self.call_stack.active_template().parents[level - 1]) + .unwrap(), + }; + + let blocks_definitions = &level_template.blocks_definitions; + + // Can we find this one block in these definitions? If so render it + if let Some(block_def) = blocks_definitions.get(&block.name) { + let (_, Block { ref body, .. }) = block_def[0]; + self.blocks.push((&block.name[..], &level_template.name[..], level)); + return self.render_body_async(body, write).await; + } + + // Do we have more parents to look through? + if level < self.call_stack.active_template().parents.len() { + return self.render_block_async(block, level + 1, write).await; + } + + // Nope, just render the body we got + self.render_body_async(&block.body, write).await + } + fn get_default_value(&mut self, expr: &'a Expr) -> Result> { if let Some(default_expr) = expr.filters[0].args.get("value") { self.eval_expression(default_expr) @@ -953,6 +1073,44 @@ impl<'a> Processor<'a> { Err(Error::msg("Tried to use super() in the top level block")) } + #[cfg(feature = "async")] + /// Only called while rendering a block. + /// This will look up the block we are currently rendering and its level and try to render + /// the block at level + n, where would be the next template in the hierarchy the block is present + /// + /// This is the async version of (`do_super`)[Self::do_super] + async fn do_super_async(&mut self, write: &mut (impl Write + Send + Sync)) -> Result<()> { + let &(block_name, _, level) = self.blocks.last().unwrap(); + let mut next_level = level + 1; + + while next_level <= self.template.parents.len() { + let blocks_definitions = &self + .tera + .get_template(&self.template.parents[next_level - 1]) + .unwrap() + .blocks_definitions; + + if let Some(block_def) = blocks_definitions.get(block_name) { + let (ref tpl_name, Block { ref body, .. }) = block_def[0]; + self.blocks.push((block_name, tpl_name, next_level)); + + self.render_body_async(body, write).await?; + self.blocks.pop(); + + // Can't go any higher for that block anymore? + if next_level >= self.template.parents.len() { + // then remove it from the stack, we're done with it + self.blocks.pop(); + } + return Ok(()); + } else { + next_level += 1; + } + } + + Err(Error::msg("Tried to use super() in the top level block")) + } + /// Looks up identifier and returns its value fn lookup_ident(&self, key: &str) -> Result> { // Magical variable that just dumps the context @@ -1041,6 +1199,95 @@ impl<'a> Processor<'a> { Ok(()) } + #[cfg(feature = "async")] + /// Process the given node asynchronously, appending the string result to the buffer + /// if it is possible + async fn render_node_async( + &mut self, + node: &'a Node, + write: &mut (impl Write + Send + Sync), + ) -> Result<()> { + match *node { + // Comments are ignored when rendering + Node::Comment(_, _) => (), + Node::Text(ref s) | Node::Raw(_, ref s, _) => write!(write, "{}", s)?, + Node::VariableBlock(_, ref expr) => self.eval_expression(expr)?.render(write)?, + Node::Set(_, ref set) => self.eval_set(set)?, + Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { + // Render to string doesnt support async yet so just do it ourselves + /* + pub(crate) fn render_to_string(context: C, render: F) -> Result + where + C: FnOnce() -> String, + F: FnOnce(&mut Vec) -> Result<(), E>, + Error: From, + { + let mut buffer = Vec::new(); + render(&mut buffer).map_err(Error::from)?; + buffer_to_string(context, buffer) + } + */ + + let mut buffer = Vec::new(); + self.render_body_async(body, &mut buffer).await?; + let body = + crate::utils::buffer_to_string(|| format!("filter {}", filter.name), buffer) + .map_err(Error::from)?; + + // the safe filter doesn't actually exist + if filter.name == "safe" { + write!(write, "{}", body)?; + } else { + self.eval_filter(&Cow::Owned(Value::String(body)), filter, &mut false)? + .render(write)?; + } + } + // Macros have been imported at the beginning + Node::ImportMacro(_, _, _) => (), + Node::If(ref if_node, _) => self.render_if_node_async(if_node, write).await?, + Node::Forloop(_, ref forloop, _) => self.render_for_loop_async(forloop, write).await?, + Node::Break(_) => { + self.call_stack.break_for_loop()?; + } + Node::Continue(_) => { + self.call_stack.continue_for_loop()?; + } + Node::Block(_, ref block, _) => self.render_block_async(block, 0, write).await?, + Node::Super => self.do_super_async(write).await?, + Node::Include(_, ref tpl_names, ignore_missing) => { + let mut found = false; + for tpl_name in tpl_names { + let template = self.tera.get_template(tpl_name); + if template.is_err() { + continue; + } + let template = template.unwrap(); + self.macros.add_macros_from_template(self.tera, template)?; + self.call_stack.push_include_frame(tpl_name, template); + self.render_body_async(&template.ast, write).await?; + self.call_stack.pop(); + found = true; + break; + } + if !found && !ignore_missing { + return Err(Error::template_not_found( + ["[", &tpl_names.join(", "), "]"].join(""), + )); + } + } + Node::Extends(_, ref name) => { + return Err(Error::msg(format!( + "Inheritance in included templates is currently not supported: extended `{}`", + name + ))); + } + // Macro definitions are ignored when rendering + Node::MacroDefinition(_, _, _) => (), + }; + + Ok(()) + } + /// Helper fn that tries to find the current context: are we in a macro? in a parent template? /// in order to give the best possible error when getting an error when rendering a tpl fn get_error_location(&self) -> String { @@ -1087,13 +1334,11 @@ impl<'a> Processor<'a> { /// Async version of [`render`](Self::render) #[cfg(feature = "async")] - pub async fn render_async(&mut self, write: &mut impl Write) -> Result<()> { + pub async fn render_async(&mut self, write: &mut (impl Write + Send + Sync)) -> Result<()> { for node in &self.template_root.ast { - self.render_node(node, write) - .map_err(|e| Error::chain(self.get_error_location(), e))?; - - // await after each node to allow the runtime to yield to other tasks - async {}.await; + self.render_node_async(node, write) + .await + .map_err(|e: Error| Error::chain(self.get_error_location(), e))?; } Ok(()) diff --git a/src/tera.rs b/src/tera.rs index 5af80130..9335fcf3 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -1142,7 +1142,7 @@ mod tests { map.insert("https://example.com", "success"); let mut tera_context = Context::new(); - tera_context.insert("map", &map); + tera_context.insert("map", &map).unwrap(); my_tera.render("dots", &tera_context).unwrap(); my_tera.render("urls", &tera_context).unwrap(); From 46ac69719cd47c9e1be4702a3f67a01cb461863f Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Sat, 13 Jul 2024 09:01:21 +0000 Subject: [PATCH 09/15] feat: add some execution constraints --- src/constraints.rs | 9 + src/lib.rs | 1 + src/renderer/call_stack.rs | 7 +- src/renderer/processor.rs | 353 +++++++++++++++++++++++++----------- src/renderer/stack_frame.rs | 7 +- 5 files changed, 269 insertions(+), 108 deletions(-) create mode 100644 src/constraints.rs diff --git a/src/constraints.rs b/src/constraints.rs new file mode 100644 index 00000000..decedafe --- /dev/null +++ b/src/constraints.rs @@ -0,0 +1,9 @@ +/// Renderer limits +pub const RENDER_BLOCK_MAX_DEPTH: usize = 5; +pub const RENDER_BODY_RECURSION_LIMIT: usize = 15; + +/// Stack frame size limit +pub const STACK_FRAME_MAX_ENTRIES: usize = 50; + +/// eval_expression max array element size +pub const EXPRESSION_MAX_ARRAY_LENGTH: usize = 100; diff --git a/src/lib.rs b/src/lib.rs index 151e530d..3029d27a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ #[macro_use] mod macros; mod builtins; +mod constraints; mod context; mod errors; mod filter_utils; diff --git a/src/renderer/call_stack.rs b/src/renderer/call_stack.rs index 90ff4ea6..181b0fdd 100644 --- a/src/renderer/call_stack.rs +++ b/src/renderer/call_stack.rs @@ -128,12 +128,13 @@ impl<'a> CallStack<'a> { } /// Add an assignment value (via {% set ... %} and {% set_global ... %} ) - pub fn add_assignment(&mut self, key: &'a str, global: bool, value: Val<'a>) { + pub fn add_assignment(&mut self, key: &'a str, global: bool, value: Val<'a>) -> Result<()> { if global { - self.global_frame_mut().insert(key, value); + self.global_frame_mut().insert(key, value)?; } else { - self.current_frame_mut().insert(key, value); + self.current_frame_mut().insert(key, value)?; } + Ok(()) } /// Breaks current for loop diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index d790fa04..32c1d1d2 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -150,9 +150,21 @@ impl<'a> Processor<'a> { } } - fn render_body(&mut self, body: &'a [Node], write: &mut impl Write) -> Result<()> { + fn render_body( + &mut self, + body: &'a [Node], + write: &mut impl Write, + body_recursion_level: usize, + ) -> Result<()> { + if body_recursion_level >= crate::constraints::RENDER_BODY_RECURSION_LIMIT { + return Err(Error::msg(format!( + "Max recursion limit reached while rendering body ({} levels)", + crate::constraints::RENDER_BODY_RECURSION_LIMIT + ))); + } + for n in body { - self.render_node(n, write)?; + self.render_node(n, write, body_recursion_level + 1)?; if self.call_stack.should_break_body() { break; @@ -168,9 +180,17 @@ impl<'a> Processor<'a> { &mut self, body: &'a [Node], write: &mut (impl Write + Send + Sync), + body_recursion_level: usize, ) -> Result<()> { + if body_recursion_level >= crate::constraints::RENDER_BODY_RECURSION_LIMIT { + return Err(Error::msg(format!( + "Max recursion limit reached while rendering body ({} levels)", + crate::constraints::RENDER_BODY_RECURSION_LIMIT + ))); + } + for n in body { - self.render_node_async(n, write).await?; + self.render_node_async(n, write, body_recursion_level + 1).await?; if self.call_stack.should_break_body() { break; @@ -182,7 +202,11 @@ impl<'a> Processor<'a> { /// Helper method to create a for loop, this makes async for loop rendering easier #[inline] - fn create_for_loop(&mut self, for_loop: &'a Forloop) -> Result> { + fn create_for_loop( + &mut self, + for_loop: &'a Forloop, + body_recursion_level: usize, + ) -> Result> { let container_name = match for_loop.container.val { ExprVal::Ident(ref ident) => ident, ExprVal::FunctionCall(FunctionCall { ref name, .. }) => name, @@ -193,7 +217,7 @@ impl<'a> Processor<'a> { ))), }; - let container_val = self.safe_eval_expression(&for_loop.container)?; + let container_val = self.safe_eval_expression(&for_loop.container, body_recursion_level)?; let for_loop = match *container_val { Value::Array(_) => { @@ -243,22 +267,27 @@ impl<'a> Processor<'a> { Ok(for_loop) } - fn render_for_loop(&mut self, for_loop: &'a Forloop, write: &mut impl Write) -> Result<()> { + fn render_for_loop( + &mut self, + for_loop: &'a Forloop, + write: &mut impl Write, + body_recursion_level: usize, + ) -> Result<()> { let for_loop_name = &for_loop.value; let for_loop_body = &for_loop.body; let for_loop_empty_body = &for_loop.empty_body; - let for_loop = self.create_for_loop(for_loop)?; + let for_loop = self.create_for_loop(for_loop, body_recursion_level)?; let len = for_loop.len(); match (len, for_loop_empty_body) { - (0, Some(empty_body)) => self.render_body(empty_body, write), + (0, Some(empty_body)) => self.render_body(empty_body, write, body_recursion_level), (0, _) => Ok(()), (_, _) => { self.call_stack.push_for_loop_frame(for_loop_name, for_loop); for _ in 0..len { - self.render_body(for_loop_body, write)?; + self.render_body(for_loop_body, write, body_recursion_level)?; if self.call_stack.should_break_for_loop() { break; @@ -279,22 +308,25 @@ impl<'a> Processor<'a> { &mut self, for_loop: &'a Forloop, write: &mut (impl Write + Send + Sync), + body_recursion_level: usize, ) -> Result<()> { let for_loop_name = &for_loop.value; let for_loop_body = &for_loop.body; let for_loop_empty_body = &for_loop.empty_body; - let for_loop = self.create_for_loop(for_loop)?; + let for_loop = self.create_for_loop(for_loop, body_recursion_level)?; let len = for_loop.len(); match (len, for_loop_empty_body) { - (0, Some(empty_body)) => self.render_body_async(empty_body, write).await, + (0, Some(empty_body)) => { + self.render_body_async(empty_body, write, body_recursion_level).await + } (0, _) => Ok(()), (_, _) => { self.call_stack.push_for_loop_frame(for_loop_name, for_loop); for _ in 0..len { - self.render_body_async(for_loop_body, write).await?; + self.render_body_async(for_loop_body, write, body_recursion_level).await?; if self.call_stack.should_break_for_loop() { break; @@ -310,15 +342,20 @@ impl<'a> Processor<'a> { } } - fn render_if_node(&mut self, if_node: &'a If, write: &mut impl Write) -> Result<()> { + fn render_if_node( + &mut self, + if_node: &'a If, + write: &mut impl Write, + body_recursion_level: usize, + ) -> Result<()> { for (_, expr, body) in &if_node.conditions { - if self.eval_as_bool(expr)? { - return self.render_body(body, write); + if self.eval_as_bool(expr, body_recursion_level)? { + return self.render_body(body, write, body_recursion_level); } } if let Some((_, ref body)) = if_node.otherwise { - return self.render_body(body, write); + return self.render_body(body, write, body_recursion_level); } Ok(()) @@ -329,15 +366,16 @@ impl<'a> Processor<'a> { &mut self, if_node: &'a If, write: &mut (impl Write + Send + Sync), + body_recursion_level: usize, ) -> Result<()> { for (_, expr, body) in &if_node.conditions { - if self.eval_as_bool(expr)? { - return self.render_body_async(body, write).await; + if self.eval_as_bool(expr, body_recursion_level)? { + return self.render_body_async(body, write, body_recursion_level).await; } } if let Some((_, ref body)) = if_node.otherwise { - return self.render_body_async(body, write).await; + return self.render_body_async(body, write, body_recursion_level).await; } Ok(()) @@ -350,8 +388,16 @@ impl<'a> Processor<'a> { &mut self, block: &'a Block, level: usize, + body_recursion_level: usize, write: &mut impl Write, ) -> Result<()> { + if level >= crate::constraints::RENDER_BLOCK_MAX_DEPTH { + return Err(Error::msg(format!( + "Max depth of block inheritance reached ({} levels)", + crate::constraints::RENDER_BLOCK_MAX_DEPTH + ))); + } + let level_template = match level { 0 => self.call_stack.active_template(), _ => self @@ -366,16 +412,16 @@ impl<'a> Processor<'a> { if let Some(block_def) = blocks_definitions.get(&block.name) { let (_, Block { ref body, .. }) = block_def[0]; self.blocks.push((&block.name[..], &level_template.name[..], level)); - return self.render_body(body, write); + return self.render_body(body, write, body_recursion_level); } // Do we have more parents to look through? if level < self.call_stack.active_template().parents.len() { - return self.render_block(block, level + 1, write); + return self.render_block(block, level + 1, body_recursion_level, write); } // Nope, just render the body we got - self.render_body(&block.body, write) + self.render_body(&block.body, write, body_recursion_level) } #[cfg(feature = "async")] @@ -389,8 +435,16 @@ impl<'a> Processor<'a> { &mut self, block: &'a Block, level: usize, + body_recursion_level: usize, write: &mut (impl Write + Send + Sync), ) -> Result<()> { + if level >= crate::constraints::RENDER_BLOCK_MAX_DEPTH { + return Err(Error::msg(format!( + "Max depth of block inheritance reached ({} levels)", + crate::constraints::RENDER_BLOCK_MAX_DEPTH + ))); + } + let level_template = match level { 0 => self.call_stack.active_template(), _ => self @@ -405,29 +459,33 @@ impl<'a> Processor<'a> { if let Some(block_def) = blocks_definitions.get(&block.name) { let (_, Block { ref body, .. }) = block_def[0]; self.blocks.push((&block.name[..], &level_template.name[..], level)); - return self.render_body_async(body, write).await; + return self.render_body_async(body, write, body_recursion_level).await; } // Do we have more parents to look through? if level < self.call_stack.active_template().parents.len() { - return self.render_block_async(block, level + 1, write).await; + return self.render_block_async(block, level + 1, body_recursion_level, write).await; } // Nope, just render the body we got - self.render_body_async(&block.body, write).await + self.render_body_async(&block.body, write, body_recursion_level).await } - fn get_default_value(&mut self, expr: &'a Expr) -> Result> { + fn get_default_value( + &mut self, + expr: &'a Expr, + body_recursion_level: usize, + ) -> Result> { if let Some(default_expr) = expr.filters[0].args.get("value") { - self.eval_expression(default_expr) + self.eval_expression(default_expr, body_recursion_level) } else { Err(Error::msg("The `default` filter requires a `value` argument.")) } } - fn eval_in_condition(&mut self, in_cond: &'a In) -> Result { - let lhs = self.safe_eval_expression(&in_cond.lhs)?; - let rhs = self.safe_eval_expression(&in_cond.rhs)?; + fn eval_in_condition(&mut self, in_cond: &'a In, body_recursion_level: usize) -> Result { + let lhs = self.safe_eval_expression(&in_cond.lhs, body_recursion_level)?; + let rhs = self.safe_eval_expression(&in_cond.rhs, body_recursion_level)?; let present = match *rhs { Value::Array(ref v) => v.contains(&lhs), @@ -459,18 +517,28 @@ impl<'a> Processor<'a> { Ok(if in_cond.negated { !present } else { present }) } - fn eval_expression(&mut self, expr: &'a Expr) -> Result> { + fn eval_expression(&mut self, expr: &'a Expr, body_recursion_level: usize) -> Result> { let mut needs_escape = false; let mut res = match expr.val { ExprVal::Array(ref arr) => { + if arr.len() > crate::constraints::EXPRESSION_MAX_ARRAY_LENGTH { + return Err(Error::msg(format!( + "Max number of elements in an array literal is {}, {:?}", + crate::constraints::EXPRESSION_MAX_ARRAY_LENGTH, + expr.val + ))); + } + let mut values = vec![]; for v in arr { - values.push(self.eval_expression(v)?.into_owned()); + values.push(self.eval_expression(v, body_recursion_level)?.into_owned()); } Cow::Owned(Value::Array(values)) } - ExprVal::In(ref in_cond) => Cow::Owned(Value::Bool(self.eval_in_condition(in_cond)?)), + ExprVal::In(ref in_cond) => { + Cow::Owned(Value::Bool(self.eval_in_condition(in_cond, body_recursion_level)?)) + } ExprVal::String(ref val) => { needs_escape = true; Cow::Owned(Value::String(val.to_string())) @@ -490,7 +558,7 @@ impl<'a> Processor<'a> { i ))), }, - ExprVal::FunctionCall(ref fn_call) => match *self.eval_tera_fn_call(fn_call, &mut needs_escape)? { + ExprVal::FunctionCall(ref fn_call) => match *self.eval_tera_fn_call(fn_call, &mut needs_escape, body_recursion_level)? { Value::String(ref v) => res.push_str(v), Value::Number(ref v) => res.push_str(&v.to_string()), _ => return Err(Error::msg(format!( @@ -514,14 +582,14 @@ impl<'a> Processor<'a> { match self.lookup_ident(ident) { Ok(val) => { if val.is_null() && expr.has_default_filter() { - self.get_default_value(expr)? + self.get_default_value(expr, body_recursion_level)? } else { val } } Err(e) => { if expr.has_default_filter() { - self.get_default_value(expr)? + self.get_default_value(expr, body_recursion_level)? } else { if !expr.negated { return Err(e); @@ -533,18 +601,22 @@ impl<'a> Processor<'a> { } } ExprVal::FunctionCall(ref fn_call) => { - self.eval_tera_fn_call(fn_call, &mut needs_escape)? + self.eval_tera_fn_call(fn_call, &mut needs_escape, body_recursion_level)? } ExprVal::MacroCall(ref macro_call) => { let val = render_to_string( || format!("macro {}", macro_call.name), - |w| self.eval_macro_call(macro_call, w), + |w| self.eval_macro_call(macro_call, w, body_recursion_level), )?; Cow::Owned(Value::String(val)) } - ExprVal::Test(ref test) => Cow::Owned(Value::Bool(self.eval_test(test)?)), - ExprVal::Logic(_) => Cow::Owned(Value::Bool(self.eval_as_bool(expr)?)), - ExprVal::Math(_) => match self.eval_as_number(&expr.val) { + ExprVal::Test(ref test) => { + Cow::Owned(Value::Bool(self.eval_test(test, body_recursion_level)?)) + } + ExprVal::Logic(_) => { + Cow::Owned(Value::Bool(self.eval_as_bool(expr, body_recursion_level)?)) + } + ExprVal::Math(_) => match self.eval_as_number(&expr.val, body_recursion_level) { Ok(Some(n)) => Cow::Owned(Value::Number(n)), Ok(None) => Cow::Owned(Value::String("NaN".to_owned())), Err(e) => return Err(Error::msg(e)), @@ -555,7 +627,7 @@ impl<'a> Processor<'a> { if filter.name == "safe" || filter.name == "default" { continue; } - res = self.eval_filter(&res, filter, &mut needs_escape)?; + res = self.eval_filter(&res, filter, &mut needs_escape, body_recursion_level)?; } // Lastly, we need to check if the expression is negated, thus turning it into a bool @@ -574,29 +646,37 @@ impl<'a> Processor<'a> { } /// Render an expression and never escape its result - fn safe_eval_expression(&mut self, expr: &'a Expr) -> Result> { + fn safe_eval_expression( + &mut self, + expr: &'a Expr, + body_recursion_level: usize, + ) -> Result> { let should_escape = self.should_escape; self.should_escape = false; - let res = self.eval_expression(expr); + let res = self.eval_expression(expr, body_recursion_level); self.should_escape = should_escape; res } /// Evaluate a set tag and add the value to the right context - fn eval_set(&mut self, set: &'a Set) -> Result<()> { - let assigned_value = self.safe_eval_expression(&set.value)?; - self.call_stack.add_assignment(&set.key[..], set.global, assigned_value); + fn eval_set(&mut self, set: &'a Set, body_recursion_level: usize) -> Result<()> { + let assigned_value = self.safe_eval_expression(&set.value, body_recursion_level)?; + self.call_stack.add_assignment(&set.key[..], set.global, assigned_value)?; Ok(()) } - fn eval_test(&mut self, test: &'a Test) -> Result { + fn eval_test(&mut self, test: &'a Test, body_recursion_level: usize) -> Result { let tester_fn = self.tera.get_tester(&test.name)?; let err_wrap = |e| Error::call_test(&test.name, e); let mut tester_args = vec![]; for arg in &test.args { - tester_args - .push(self.safe_eval_expression(arg).map_err(err_wrap)?.clone().into_owned()); + tester_args.push( + self.safe_eval_expression(arg, body_recursion_level) + .map_err(err_wrap)? + .clone() + .into_owned(), + ); } let found = self.lookup_ident(&test.ident).map(|found| found.clone().into_owned()).ok(); @@ -613,6 +693,7 @@ impl<'a> Processor<'a> { &mut self, function_call: &'a FunctionCall, needs_escape: &mut bool, + body_recursion_level: usize, ) -> Result> { let tera_fn = self.tera.get_function(&function_call.name)?; *needs_escape = !tera_fn.is_safe(); @@ -623,14 +704,22 @@ impl<'a> Processor<'a> { for (arg_name, expr) in &function_call.args { args.insert( arg_name.to_string(), - self.safe_eval_expression(expr).map_err(err_wrap)?.clone().into_owned(), + self.safe_eval_expression(expr, body_recursion_level) + .map_err(err_wrap)? + .clone() + .into_owned(), ); } Ok(Cow::Owned(tera_fn.call(&args).map_err(err_wrap)?)) } - fn eval_macro_call(&mut self, macro_call: &'a MacroCall, write: &mut impl Write) -> Result<()> { + fn eval_macro_call( + &mut self, + macro_call: &'a MacroCall, + write: &mut impl Write, + body_recursion_level: usize, + ) -> Result<()> { let active_template_name = if let Some(block) = self.blocks.last() { block.1 } else if self.template.name != self.template_root.name { @@ -650,9 +739,9 @@ impl<'a> Processor<'a> { // First the default arguments for (arg_name, default_value) in ¯o_definition.args { let value = match macro_call.args.get(arg_name) { - Some(val) => self.safe_eval_expression(val)?, + Some(val) => self.safe_eval_expression(val, body_recursion_level)?, None => match *default_value { - Some(ref val) => self.safe_eval_expression(val)?, + Some(ref val) => self.safe_eval_expression(val, body_recursion_level)?, None => { return Err(Error::msg(format!( "Macro `{}` is missing the argument `{}`", @@ -671,7 +760,7 @@ impl<'a> Processor<'a> { self.tera.get_template(macro_template_name)?, ); - self.render_body(¯o_definition.body, write)?; + self.render_body(¯o_definition.body, write, body_recursion_level)?; self.call_stack.pop(); @@ -683,6 +772,7 @@ impl<'a> Processor<'a> { value: &Val<'a>, fn_call: &'a FunctionCall, needs_escape: &mut bool, + body_recursion_level: usize, ) -> Result> { let filter_fn = self.tera.get_filter(&fn_call.name)?; *needs_escape = !filter_fn.is_safe(); @@ -693,25 +783,34 @@ impl<'a> Processor<'a> { for (arg_name, expr) in &fn_call.args { args.insert( arg_name.to_string(), - self.safe_eval_expression(expr).map_err(err_wrap)?.clone().into_owned(), + self.safe_eval_expression(expr, body_recursion_level) + .map_err(err_wrap)? + .clone() + .into_owned(), ); } Ok(Cow::Owned(filter_fn.filter(value, &args).map_err(err_wrap)?)) } - fn eval_as_bool(&mut self, bool_expr: &'a Expr) -> Result { + fn eval_as_bool(&mut self, bool_expr: &'a Expr, body_recursion_level: usize) -> Result { let res = match bool_expr.val { ExprVal::Logic(LogicExpr { ref lhs, ref rhs, ref operator }) => { match *operator { - LogicOperator::Or => self.eval_as_bool(lhs)? || self.eval_as_bool(rhs)?, - LogicOperator::And => self.eval_as_bool(lhs)? && self.eval_as_bool(rhs)?, + LogicOperator::Or => { + self.eval_as_bool(lhs, body_recursion_level)? + || self.eval_as_bool(rhs, body_recursion_level)? + } + LogicOperator::And => { + self.eval_as_bool(lhs, body_recursion_level)? + && self.eval_as_bool(rhs, body_recursion_level)? + } LogicOperator::Gt | LogicOperator::Gte | LogicOperator::Lt | LogicOperator::Lte => { - let l = self.eval_expr_as_number(lhs)?; - let r = self.eval_expr_as_number(rhs)?; + let l = self.eval_expr_as_number(lhs, body_recursion_level)?; + let r = self.eval_expr_as_number(rhs, body_recursion_level)?; let (ll, rr) = match (l, r) { (Some(nl), Some(nr)) => (nl, nr), _ => return Err(Error::msg("Comparison to NaN")), @@ -731,8 +830,8 @@ impl<'a> Processor<'a> { } } LogicOperator::Eq | LogicOperator::NotEq => { - let mut lhs_val = self.eval_expression(lhs)?; - let mut rhs_val = self.eval_expression(rhs)?; + let mut lhs_val = self.eval_expression(lhs, body_recursion_level)?; + let mut rhs_val = self.eval_expression(rhs, body_recursion_level)?; // Monomorphize number vals. if lhs_val.is_number() || rhs_val.is_number() { @@ -764,7 +863,7 @@ impl<'a> Processor<'a> { } ExprVal::Ident(_) => { let mut res = self - .eval_expression(bool_expr) + .eval_expression(bool_expr, body_recursion_level) .unwrap_or(Cow::Owned(Value::Bool(false))) .is_truthy(); if bool_expr.negated { @@ -773,17 +872,17 @@ impl<'a> Processor<'a> { res } ExprVal::Math(_) | ExprVal::Int(_) | ExprVal::Float(_) => { - match self.eval_as_number(&bool_expr.val)? { + match self.eval_as_number(&bool_expr.val, body_recursion_level)? { Some(n) => n.as_f64().unwrap() != 0.0, None => false, } } - ExprVal::In(ref in_cond) => self.eval_in_condition(in_cond)?, - ExprVal::Test(ref test) => self.eval_test(test)?, + ExprVal::In(ref in_cond) => self.eval_in_condition(in_cond, body_recursion_level)?, + ExprVal::Test(ref test) => self.eval_test(test, body_recursion_level)?, ExprVal::Bool(val) => val, ExprVal::String(ref string) => !string.is_empty(), ExprVal::FunctionCall(ref fn_call) => { - let v = self.eval_tera_fn_call(fn_call, &mut false)?; + let v = self.eval_tera_fn_call(fn_call, &mut false, body_recursion_level)?; match v.as_bool() { Some(val) => val, None => { @@ -795,12 +894,12 @@ impl<'a> Processor<'a> { } } ExprVal::StringConcat(_) => { - let res = self.eval_expression(bool_expr)?; + let res = self.eval_expression(bool_expr, body_recursion_level)?; !res.as_str().unwrap().is_empty() } ExprVal::MacroCall(ref macro_call) => { let mut buf = Vec::new(); - self.eval_macro_call(macro_call, &mut buf)?; + self.eval_macro_call(macro_call, &mut buf, body_recursion_level)?; !buf.is_empty() } _ => { @@ -820,21 +919,29 @@ impl<'a> Processor<'a> { /// In some cases, we will have filters in lhs/rhs of a math expression /// `eval_as_number` only works on ExprVal rather than Expr - fn eval_expr_as_number(&mut self, expr: &'a Expr) -> Result> { + fn eval_expr_as_number( + &mut self, + expr: &'a Expr, + body_recursion_level: usize, + ) -> Result> { if !expr.filters.is_empty() { - match *self.eval_expression(expr)? { + match *self.eval_expression(expr, body_recursion_level)? { Value::Number(ref s) => Ok(Some(s.clone())), _ => { Err(Error::msg("Tried to do math with an expression not resulting in a number")) } } } else { - self.eval_as_number(&expr.val) + self.eval_as_number(&expr.val, body_recursion_level) } } /// Return the value of an expression as a number - fn eval_as_number(&mut self, expr: &'a ExprVal) -> Result> { + fn eval_as_number( + &mut self, + expr: &'a ExprVal, + body_recursion_level: usize, + ) -> Result> { let result = match *expr { ExprVal::Ident(ref ident) => { let v = &*self.lookup_ident(ident)?; @@ -854,8 +961,10 @@ impl<'a> Processor<'a> { ExprVal::Int(val) => Some(Number::from(val)), ExprVal::Float(val) => Some(Number::from_f64(val).unwrap()), ExprVal::Math(MathExpr { ref lhs, ref rhs, ref operator }) => { - let (l, r) = match (self.eval_expr_as_number(lhs)?, self.eval_expr_as_number(rhs)?) - { + let (l, r) = match ( + self.eval_expr_as_number(lhs, body_recursion_level)?, + self.eval_expr_as_number(rhs, body_recursion_level)?, + ) { (Some(l), Some(r)) => (l, r), _ => return Ok(None), }; @@ -1003,7 +1112,7 @@ impl<'a> Processor<'a> { } } ExprVal::FunctionCall(ref fn_call) => { - let v = self.eval_tera_fn_call(fn_call, &mut false)?; + let v = self.eval_tera_fn_call(fn_call, &mut false, body_recursion_level)?; if v.is_i64() { Some(Number::from(v.as_i64().unwrap())) } else if v.is_u64() { @@ -1041,7 +1150,7 @@ impl<'a> Processor<'a> { /// Only called while rendering a block. /// This will look up the block we are currently rendering and its level and try to render /// the block at level + n, where would be the next template in the hierarchy the block is present - fn do_super(&mut self, write: &mut impl Write) -> Result<()> { + fn do_super(&mut self, write: &mut impl Write, body_recursion_level: usize) -> Result<()> { let &(block_name, _, level) = self.blocks.last().unwrap(); let mut next_level = level + 1; @@ -1056,7 +1165,7 @@ impl<'a> Processor<'a> { let (ref tpl_name, Block { ref body, .. }) = block_def[0]; self.blocks.push((block_name, tpl_name, next_level)); - self.render_body(body, write)?; + self.render_body(body, write, body_recursion_level)?; self.blocks.pop(); // Can't go any higher for that block anymore? @@ -1079,7 +1188,11 @@ impl<'a> Processor<'a> { /// the block at level + n, where would be the next template in the hierarchy the block is present /// /// This is the async version of (`do_super`)[Self::do_super] - async fn do_super_async(&mut self, write: &mut (impl Write + Send + Sync)) -> Result<()> { + async fn do_super_async( + &mut self, + write: &mut (impl Write + Send + Sync), + body_recursion_level: usize, + ) -> Result<()> { let &(block_name, _, level) = self.blocks.last().unwrap(); let mut next_level = level + 1; @@ -1094,7 +1207,7 @@ impl<'a> Processor<'a> { let (ref tpl_name, Block { ref body, .. }) = block_def[0]; self.blocks.push((block_name, tpl_name, next_level)); - self.render_body_async(body, write).await?; + self.render_body_async(body, write, body_recursion_level).await?; self.blocks.pop(); // Can't go any higher for that block anymore? @@ -1133,38 +1246,56 @@ impl<'a> Processor<'a> { /// Process the given node, appending the string result to the buffer /// if it is possible - fn render_node(&mut self, node: &'a Node, write: &mut impl Write) -> Result<()> { + fn render_node( + &mut self, + node: &'a Node, + write: &mut impl Write, + body_recursion_level: usize, // Must be tracked to avoid infinite recursion + ) -> Result<()> { match *node { // Comments are ignored when rendering Node::Comment(_, _) => (), Node::Text(ref s) | Node::Raw(_, ref s, _) => write!(write, "{}", s)?, - Node::VariableBlock(_, ref expr) => self.eval_expression(expr)?.render(write)?, - Node::Set(_, ref set) => self.eval_set(set)?, + Node::VariableBlock(_, ref expr) => { + self.eval_expression(expr, body_recursion_level)?.render(write)? + } + Node::Set(_, ref set) => self.eval_set(set, body_recursion_level)?, Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { let body = render_to_string( || format!("filter {}", filter.name), - |w| self.render_body(body, w), + |w| self.render_body(body, w, body_recursion_level), )?; // the safe filter doesn't actually exist if filter.name == "safe" { write!(write, "{}", body)?; } else { - self.eval_filter(&Cow::Owned(Value::String(body)), filter, &mut false)? - .render(write)?; + self.eval_filter( + &Cow::Owned(Value::String(body)), + filter, + &mut false, + body_recursion_level, + )? + .render(write)?; } } // Macros have been imported at the beginning Node::ImportMacro(_, _, _) => (), - Node::If(ref if_node, _) => self.render_if_node(if_node, write)?, - Node::Forloop(_, ref forloop, _) => self.render_for_loop(forloop, write)?, + Node::If(ref if_node, _) => { + self.render_if_node(if_node, write, body_recursion_level)? + } + Node::Forloop(_, ref forloop, _) => { + self.render_for_loop(forloop, write, body_recursion_level)? + } Node::Break(_) => { self.call_stack.break_for_loop()?; } Node::Continue(_) => { self.call_stack.continue_for_loop()?; } - Node::Block(_, ref block, _) => self.render_block(block, 0, write)?, - Node::Super => self.do_super(write)?, + Node::Block(_, ref block, _) => { + self.render_block(block, 0, body_recursion_level, write)? + } + Node::Super => self.do_super(write, body_recursion_level)?, Node::Include(_, ref tpl_names, ignore_missing) => { let mut found = false; for tpl_name in tpl_names { @@ -1175,7 +1306,7 @@ impl<'a> Processor<'a> { let template = template.unwrap(); self.macros.add_macros_from_template(self.tera, template)?; self.call_stack.push_include_frame(tpl_name, template); - self.render_body(&template.ast, write)?; + self.render_body(&template.ast, write, body_recursion_level)?; self.call_stack.pop(); found = true; break; @@ -1206,13 +1337,16 @@ impl<'a> Processor<'a> { &mut self, node: &'a Node, write: &mut (impl Write + Send + Sync), + body_recursion_level: usize, // Must be tracked to avoid infinite recursion ) -> Result<()> { match *node { // Comments are ignored when rendering Node::Comment(_, _) => (), Node::Text(ref s) | Node::Raw(_, ref s, _) => write!(write, "{}", s)?, - Node::VariableBlock(_, ref expr) => self.eval_expression(expr)?.render(write)?, - Node::Set(_, ref set) => self.eval_set(set)?, + Node::VariableBlock(_, ref expr) => { + self.eval_expression(expr, body_recursion_level)?.render(write)? + } + Node::Set(_, ref set) => self.eval_set(set, body_recursion_level)?, Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { // Render to string doesnt support async yet so just do it ourselves /* @@ -1229,7 +1363,7 @@ impl<'a> Processor<'a> { */ let mut buffer = Vec::new(); - self.render_body_async(body, &mut buffer).await?; + self.render_body_async(body, &mut buffer, body_recursion_level).await?; let body = crate::utils::buffer_to_string(|| format!("filter {}", filter.name), buffer) .map_err(Error::from)?; @@ -1238,22 +1372,33 @@ impl<'a> Processor<'a> { if filter.name == "safe" { write!(write, "{}", body)?; } else { - self.eval_filter(&Cow::Owned(Value::String(body)), filter, &mut false)? - .render(write)?; + self.eval_filter( + &Cow::Owned(Value::String(body)), + filter, + &mut false, + body_recursion_level, + )? + .render(write)?; } } // Macros have been imported at the beginning Node::ImportMacro(_, _, _) => (), - Node::If(ref if_node, _) => self.render_if_node_async(if_node, write).await?, - Node::Forloop(_, ref forloop, _) => self.render_for_loop_async(forloop, write).await?, + Node::If(ref if_node, _) => { + self.render_if_node_async(if_node, write, body_recursion_level).await? + } + Node::Forloop(_, ref forloop, _) => { + self.render_for_loop_async(forloop, write, body_recursion_level).await? + } Node::Break(_) => { self.call_stack.break_for_loop()?; } Node::Continue(_) => { self.call_stack.continue_for_loop()?; } - Node::Block(_, ref block, _) => self.render_block_async(block, 0, write).await?, - Node::Super => self.do_super_async(write).await?, + Node::Block(_, ref block, _) => { + self.render_block_async(block, 0, body_recursion_level, write).await? + } + Node::Super => self.do_super_async(write, body_recursion_level).await?, Node::Include(_, ref tpl_names, ignore_missing) => { let mut found = false; for tpl_name in tpl_names { @@ -1264,7 +1409,7 @@ impl<'a> Processor<'a> { let template = template.unwrap(); self.macros.add_macros_from_template(self.tera, template)?; self.call_stack.push_include_frame(tpl_name, template); - self.render_body_async(&template.ast, write).await?; + self.render_body_async(&template.ast, write, body_recursion_level).await?; self.call_stack.pop(); found = true; break; @@ -1325,7 +1470,7 @@ impl<'a> Processor<'a> { /// Entry point for the rendering pub fn render(&mut self, write: &mut impl Write) -> Result<()> { for node in &self.template_root.ast { - self.render_node(node, write) + self.render_node(node, write, 0) .map_err(|e| Error::chain(self.get_error_location(), e))?; } @@ -1336,7 +1481,7 @@ impl<'a> Processor<'a> { #[cfg(feature = "async")] pub async fn render_async(&mut self, write: &mut (impl Write + Send + Sync)) -> Result<()> { for node in &self.template_root.ast { - self.render_node_async(node, write) + self.render_node_async(node, write, 0) .await .map_err(|e: Error| Error::chain(self.get_error_location(), e))?; } diff --git a/src/renderer/stack_frame.rs b/src/renderer/stack_frame.rs index 59a2a0aa..cf877d73 100644 --- a/src/renderer/stack_frame.rs +++ b/src/renderer/stack_frame.rs @@ -1,3 +1,4 @@ +use crate::errors::{Error, Result}; use std::borrow::Cow; use std::collections::HashMap; @@ -177,8 +178,12 @@ impl<'a> StackFrame<'a> { } /// Insert a value in the context - pub fn insert(&mut self, key: &'a str, value: Val<'a>) { + pub fn insert(&mut self, key: &'a str, value: Val<'a>) -> Result<()> { + if self.context.len() >= crate::constraints::STACK_FRAME_MAX_ENTRIES { + return Err(Error::msg("Stack frame context is full")); + } self.context.insert(key, value); + Ok(()) } /// Context is cleared on each loop From 1f078483d43e34d8defeb9dc10bed2a7657abcc2 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Sat, 13 Jul 2024 09:19:30 +0000 Subject: [PATCH 10/15] constraint: add constraint on range function --- src/builtins/functions.rs | 10 ++++++++++ src/constraints.rs | 3 +++ 2 files changed, 13 insertions(+) diff --git a/src/builtins/functions.rs b/src/builtins/functions.rs index a2690e29..763f4775 100644 --- a/src/builtins/functions.rs +++ b/src/builtins/functions.rs @@ -74,6 +74,16 @@ pub fn range(args: &HashMap) -> Result { )); } + let expected_num_elements = (end - start) / step_by; + + if expected_num_elements > crate::constraints::RANGE_MAX_ELEMENTS { + return Err(Error::msg(format!( + "Function `range` was called with a range that would generate {} elements, but the maximum allowed is {}", + expected_num_elements, + crate::constraints::RANGE_MAX_ELEMENTS + ))); + } + let mut i = start; let mut res = vec![]; while i < end { diff --git a/src/constraints.rs b/src/constraints.rs index decedafe..15ebbb36 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -7,3 +7,6 @@ pub const STACK_FRAME_MAX_ENTRIES: usize = 50; /// eval_expression max array element size pub const EXPRESSION_MAX_ARRAY_LENGTH: usize = 100; + +// range max elements +pub const RANGE_MAX_ELEMENTS: usize = 500; From e2f45b5a901b954afb432d87892fcfd43141b2a2 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Wed, 17 Jul 2024 11:20:45 +0000 Subject: [PATCH 11/15] handle divisions by u64/i64 better --- src/renderer/processor.rs | 54 +++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index 32c1d1d2..c7b97439 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -1001,19 +1001,57 @@ impl<'a> Processor<'a> { } else { let ll = l.as_f64().unwrap(); let rr = r.as_f64().unwrap(); + Number::from_f64(ll * rr) } } MathOperator::Div => { - let ll = l.as_f64().unwrap(); - let rr = r.as_f64().unwrap(); - let res = ll / rr; - if res.is_nan() { - None - } else if res.round() == res && res.is_finite() { - Some(Number::from(res as i64)) + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + + match ll.checked_div(rr) { + Some(s) => Some(Number::from(s)), + None => { + return Err(Error::msg(format!( + "{} / {} results in an out of bounds i64 or division by zero", + ll, rr + ))); + } + } + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + + match ll.checked_div(rr) { + Some(s) => Some(Number::from(s)), + None => { + return Err(Error::msg(format!( + "{} / {} results in an out of bounds u64 or division by zero", + ll, rr + ))); + } + } } else { - Number::from_f64(res) + let ll = l.as_f64().unwrap(); + let rr = r.as_f64().unwrap(); + + if rr == 0.0 { + return Err(Error::msg(format!( + "Tried to divide by zero: {:?}/{:?}", + lhs, rhs + ))); + } + + let res = ll / rr; + + if res.is_nan() { + None + } else if res.round() == res && res.is_finite() { + Some(Number::from(res as i64)) + } else { + Number::from_f64(res) + } } } MathOperator::Add => { From f359b42d9988e8cf30801cf121833d8a9b3ad5cb Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Wed, 24 Jul 2024 14:51:35 +0000 Subject: [PATCH 12/15] add bitwise operators to tera --- src/builtins/filters/object.rs | 48 ++++++++++++- src/parser/ast.rs | 28 +++++++- src/parser/mod.rs | 28 ++++++-- src/parser/tera.pest | 12 +++- src/parser/tests/parser.rs | 89 +++++++++++++++++++++++ src/renderer/processor.rs | 127 ++++++++++++++++++++++++++++++++- src/tera.rs | 1 + 7 files changed, 321 insertions(+), 12 deletions(-) diff --git a/src/builtins/filters/object.rs b/src/builtins/filters/object.rs index f9981450..55555f14 100644 --- a/src/builtins/filters/object.rs +++ b/src/builtins/filters/object.rs @@ -1,7 +1,7 @@ /// Filters operating on numbers use std::collections::HashMap; -use serde_json::value::Value; +use serde_json::{to_value, value::Value}; use crate::errors::{Error, Result}; @@ -29,6 +29,29 @@ pub fn get(value: &Value, args: &HashMap) -> Result { } } +/// Merge two objects, the second object is indicated by the `with` argument. +/// The second object's values will overwrite the first's in the event of a key conflict. +pub fn merge(value: &Value, args: &HashMap) -> Result { + let left = match value.as_object() { + Some(val) => val, + None => return Err(Error::msg("Filter `merge` was used on a value that isn't an object")), + }; + let with = match args.get("with") { + Some(val) => val, + None => return Err(Error::msg("The `merge` filter has to have a `with` argument")), + }; + match with.as_object() { + Some(right) => { + let mut result = left.clone(); + result.extend(right.clone()); + // We've already confirmed both sides were HashMaps, the result is a HashMap - + // - so unwrap + Ok(to_value(result).unwrap()) + } + None => Err(Error::msg("The `with` argument for the `get` filter must be an object")), + } +} + #[cfg(test)] mod tests { use super::*; @@ -87,4 +110,27 @@ mod tests { assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("default").unwrap()); } + + #[test] + fn test_merge_filter() { + let mut obj_1 = HashMap::new(); + obj_1.insert("1".to_string(), "first".to_string()); + obj_1.insert("2".to_string(), "second".to_string()); + + let mut obj_2 = HashMap::new(); + obj_2.insert("2".to_string(), "SECOND".to_string()); + obj_2.insert("3".to_string(), "third".to_string()); + + let mut args = HashMap::new(); + args.insert("with".to_string(), to_value(obj_2).unwrap()); + + let result = merge(&to_value(&obj_1).unwrap(), &args); + assert!(result.is_ok()); + + let mut expected = HashMap::new(); + expected.insert("1".to_string(), "first".to_string()); + expected.insert("2".to_string(), "SECOND".to_string()); + expected.insert("3".to_string(), "third".to_string()); + assert_eq!(result.unwrap(), to_value(expected).unwrap()); + } } diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 1ff895c0..fe2a5cb0 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -23,6 +23,16 @@ pub enum MathOperator { Div, /// % Modulo, + /// bitor (bitwise or) + BitOr, + /// bitxor (bitwise xor) + BitXor, + /// bitand (bitwise and) + BitAnd, + /// bitlshift (<<) + BitLeftShift, + /// bitrshift (>>) + BitRightShift, } impl fmt::Display for MathOperator { @@ -36,6 +46,11 @@ impl fmt::Display for MathOperator { MathOperator::Mul => "*", MathOperator::Div => "/", MathOperator::Modulo => "%", + MathOperator::BitOr => "bitor", + MathOperator::BitXor => "bitxor", + MathOperator::BitAnd => "bitand", + MathOperator::BitLeftShift => "bitlshift", + MathOperator::BitRightShift => "bitrshift", } ) } @@ -174,6 +189,8 @@ pub struct Expr { pub val: ExprVal, /// Is it using `not`? pub negated: bool, + /// Is it using `bitnot` + pub bitnot: bool, /// List of filters used on that value pub filters: Vec, } @@ -181,17 +198,22 @@ pub struct Expr { impl Expr { /// Create a new basic Expr pub fn new(val: ExprVal) -> Expr { - Expr { val, negated: false, filters: vec![] } + Expr { val, negated: false, bitnot: false, filters: vec![] } } /// Create a new negated Expr pub fn new_negated(val: ExprVal) -> Expr { - Expr { val, negated: true, filters: vec![] } + Expr { val, negated: true, bitnot: false, filters: vec![] } + } + + /// Create a new bitnot Expr + pub fn new_bitnot(val: ExprVal) -> Expr { + Expr { val, negated: false, bitnot: true, filters: vec![] } } /// Create a new basic Expr with some filters pub fn with_filters(val: ExprVal, filters: Vec) -> Expr { - Expr { val, filters, negated: false } + Expr { val, filters, negated: false, bitnot: false } } /// Check if the expr has a default filter as first filter diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5dffab08..e16cf38d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -29,6 +29,7 @@ pub use self::whitespace::remove_whitespace; lazy_static! { static ref MATH_PARSER: PrattParser = PrattParser::new() .op(Op::infix(Rule::op_plus, Assoc::Left) | Op::infix(Rule::op_minus, Assoc::Left)) // +, - + .op(Op::infix(Rule::op_bitor, Assoc::Left) | Op::infix(Rule::op_bitxor, Assoc::Left) | Op::infix(Rule::op_bitand, Assoc::Left) | Op::infix(Rule::op_bitlshift, Assoc::Left) | Op::infix(Rule::op_bitrshift, Assoc::Left)) // bitor, bitxor, bitand, bitlshift, bitrshift .op(Op::infix(Rule::op_times, Assoc::Left) | Op::infix(Rule::op_slash, Assoc::Left) | Op::infix(Rule::op_modulo, Assoc::Left)); // *, /, % @@ -39,7 +40,7 @@ lazy_static! { | Op::infix(Rule::op_eq, Assoc::Left)| Op::infix(Rule::op_ineq, Assoc::Left)); // <, <=, >, >=, ==, != static ref LOGIC_EXPR_PARSER: PrattParser = PrattParser::new() - .op(Op::infix(Rule::op_or, Assoc::Left)).op(Op::infix(Rule::op_and, Assoc::Left)); + .op(Op::infix(Rule::op_or, Assoc::Left)).op(Op::infix(Rule::op_and, Assoc::Left)); // or, and } /// Strings are delimited by double quotes, single quotes and backticks @@ -263,6 +264,11 @@ fn parse_basic_expression(pair: Pair) -> TeraResult { Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, + Rule::op_bitor => MathOperator::BitOr, + Rule::op_bitxor => MathOperator::BitXor, + Rule::op_bitand => MathOperator::BitAnd, + Rule::op_bitlshift => MathOperator::BitLeftShift, + Rule::op_bitrshift => MathOperator::BitRightShift, _ => { return Err(Error::msg(format!("Unexpected rule in infix: {:?}", op.as_rule()))) } @@ -329,7 +335,7 @@ fn parse_basic_expr_with_filters(pair: Pair) -> TeraResult { }; } - Ok(Expr { val: expr_val.unwrap(), negated: false, filters }) + Ok(Expr { val: expr_val.unwrap(), negated: false, bitnot: false, filters }) } /// A string expression with optional filters @@ -351,7 +357,7 @@ fn parse_string_expr_with_filters(pair: Pair) -> TeraResult { }; } - Ok(Expr { val: expr_val.unwrap(), negated: false, filters }) + Ok(Expr { val: expr_val.unwrap(), negated: false, bitnot: false, filters }) } /// An array with optional filters @@ -372,7 +378,7 @@ fn parse_array_with_filters(pair: Pair) -> TeraResult { }; } - Ok(Expr { val: array.unwrap(), negated: false, filters }) + Ok(Expr { val: array.unwrap(), negated: false, bitnot: false, filters }) } fn parse_in_condition_container(pair: Pair) -> TeraResult { @@ -437,6 +443,11 @@ fn parse_comparison_val(pair: Pair) -> TeraResult { Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, + Rule::op_bitor => MathOperator::BitOr, + Rule::op_bitxor => MathOperator::BitXor, + Rule::op_bitand => MathOperator::BitAnd, + Rule::op_bitlshift => MathOperator::BitLeftShift, + Rule::op_bitrshift => MathOperator::BitRightShift, _ => { return Err(Error::msg(format!( "PARSER ERROR: Unexpected rule in infix: {:?}", @@ -506,11 +517,13 @@ fn parse_comparison_expression(pair: Pair) -> TeraResult { /// An expression that can be negated fn parse_logic_val(pair: Pair) -> TeraResult { let mut negated = false; + let mut bitnot = false; let mut expr = None; for p in pair.into_inner() { match p.as_rule() { Rule::op_not => negated = true, + Rule::op_bitnot => bitnot = true, Rule::in_cond => expr = Some(parse_in_condition(p)?), Rule::comparison_expr => expr = Some(parse_comparison_expression(p)?), Rule::string_expr_filter => expr = Some(parse_string_expr_with_filters(p)?), @@ -526,6 +539,7 @@ fn parse_logic_val(pair: Pair) -> TeraResult { let mut e = expr.unwrap(); e.negated = negated; + e.bitnot = bitnot; Ok(e) } @@ -1319,6 +1333,7 @@ pub fn parse(input: &str) -> TeraResult> { Rule::op_or => "`or`".to_string(), Rule::op_and => "`and`".to_string(), Rule::op_not => "`not`".to_string(), + Rule::op_bitnot => "`bitnot`".to_string(), Rule::op_lte => "`<=`".to_string(), Rule::op_gte => "`>=`".to_string(), Rule::op_lt => "`<`".to_string(), @@ -1330,6 +1345,11 @@ pub fn parse(input: &str) -> TeraResult> { Rule::op_times => "`*`".to_string(), Rule::op_slash => "`/`".to_string(), Rule::op_modulo => "`%`".to_string(), + Rule::op_bitor => "`bitor`".to_string(), + Rule::op_bitxor => "`bitxor`".to_string(), + Rule::op_bitand => "`bitand`".to_string(), + Rule::op_bitlshift => "`bitlshift`".to_string(), + Rule::op_bitrshift => "`bitrshift`".to_string(), Rule::filter => "a filter".to_string(), Rule::test => "a test".to_string(), Rule::test_not => "a negated test".to_string(), diff --git a/src/parser/tera.pest b/src/parser/tera.pest index 03c74748..2d754791 100644 --- a/src/parser/tera.pest +++ b/src/parser/tera.pest @@ -35,6 +35,7 @@ boolean = { "true" | "false" | "True" | "False" } op_or = @{ "or" ~ WHITESPACE } op_and = @{ "and" ~ WHITESPACE } op_not = @{ "not" ~ WHITESPACE } +op_bitnot = @{ "bitnot" ~ WHITESPACE } // bitwise ones complement cannot use ~ op_lte = { "<=" } op_gte = { ">=" } op_lt = { "<" } @@ -46,6 +47,11 @@ op_minus = { "-" } op_times = { "*" } op_slash = { "/" } op_modulo = { "%" } +op_bitor = { "bitor" } // bitwise or cannot use | +op_bitxor = { "bitxor" } // bitwise xor cannot use ^ +op_bitand = { "bitand" } // bitwise and cannot use & +op_bitlshift = { "<<" } +op_bitrshift = { ">>" } // ------------------------------------------------- @@ -85,7 +91,7 @@ string_concat = { (fn_call | float | int | string | dotted_square_bracket_ident) // boolean first so they are not caught as identifiers basic_val = _{ boolean | test_not | test | macro_call | fn_call | dotted_square_bracket_ident | float | int } -basic_op = _{ op_plus | op_minus | op_times | op_slash | op_modulo } +basic_op = _{ op_plus | op_minus | op_times | op_slash | op_modulo | op_bitor | op_bitxor | op_bitand | op_bitlshift | op_bitrshift } basic_expr = { ("(" ~ basic_expr ~ ")" | basic_val) ~ (basic_op ~ ("(" ~ basic_expr ~ ")" | basic_val))* } basic_expr_filter = !{ basic_expr ~ filter* } string_expr_filter = !{ (string_concat | string) ~ filter* } @@ -98,8 +104,8 @@ comparison_expr = { (string_expr_filter | comparison_val) ~ (comparison_op ~ (st in_cond_container = {string_expr_filter | array_filter | dotted_square_bracket_ident} in_cond = !{ (string_expr_filter | basic_expr_filter) ~ op_not? ~ "in" ~ in_cond_container } -logic_val = !{ op_not? ~ (in_cond | comparison_expr) | "(" ~ logic_expr ~ ")" } -logic_expr = !{ logic_val ~ ((op_or | op_and) ~ logic_val)* } +logic_val = !{ (op_not | op_bitnot)? ~ (in_cond | comparison_expr) | "(" ~ logic_expr ~ ")" } +logic_expr = !{ logic_val ~ ((op_or | op_and ) ~ logic_val)* } array = !{ "[" ~ (logic_val ~ ",")* ~ logic_val? ~ "]"} array_filter = !{ array ~ filter* } diff --git a/src/parser/tests/parser.rs b/src/parser/tests/parser.rs index 73db69b2..f0c0b193 100644 --- a/src/parser/tests/parser.rs +++ b/src/parser/tests/parser.rs @@ -360,6 +360,95 @@ fn parse_variable_tag_negated_expr() { ); } +#[test] +fn parse_variable_tag_bitor() { + let ast = parse("{{ id bitor id2 }}").unwrap(); + assert_eq!( + ast[0], + Node::VariableBlock( + WS::default(), + Expr::new(ExprVal::Math(MathExpr { + lhs: Box::new(Expr::new(ExprVal::Ident("id".to_string()))), + operator: MathOperator::BitOr, + rhs: Box::new(Expr::new(ExprVal::Ident("id2".to_string()))), + },)) + ) + ); +} + +#[test] +fn parse_variable_tag_bitxor() { + let ast = parse("{{ id bitxor id2 }}").unwrap(); + assert_eq!( + ast[0], + Node::VariableBlock( + WS::default(), + Expr::new(ExprVal::Math(MathExpr { + lhs: Box::new(Expr::new(ExprVal::Ident("id".to_string()))), + operator: MathOperator::BitXor, + rhs: Box::new(Expr::new(ExprVal::Ident("id2".to_string()))), + },)) + ) + ); +} + +#[test] +fn parse_variable_tag_bitand() { + let ast = parse("{{ id bitand id2 }}").unwrap(); + assert_eq!( + ast[0], + Node::VariableBlock( + WS::default(), + Expr::new(ExprVal::Math(MathExpr { + lhs: Box::new(Expr::new(ExprVal::Ident("id".to_string()))), + operator: MathOperator::BitAnd, + rhs: Box::new(Expr::new(ExprVal::Ident("id2".to_string()))), + },)) + ) + ); +} + +#[test] +fn parse_variable_tag_bitlshift() { + let ast = parse("{{ id << id2 }}").unwrap(); + assert_eq!( + ast[0], + Node::VariableBlock( + WS::default(), + Expr::new(ExprVal::Math(MathExpr { + lhs: Box::new(Expr::new(ExprVal::Ident("id".to_string()))), + operator: MathOperator::BitLeftShift, + rhs: Box::new(Expr::new(ExprVal::Ident("id2".to_string()))), + },)) + ) + ); +} + +#[test] +fn parse_variable_tag_bitrshift() { + let ast = parse("{{ id >> id2 }}").unwrap(); + assert_eq!( + ast[0], + Node::VariableBlock( + WS::default(), + Expr::new(ExprVal::Math(MathExpr { + lhs: Box::new(Expr::new(ExprVal::Ident("id".to_string()))), + operator: MathOperator::BitRightShift, + rhs: Box::new(Expr::new(ExprVal::Ident("id2".to_string()))), + },)) + ) + ); +} + +#[test] +fn parse_variable_tag_bitnot() { + let ast = parse("{{ bitnot id }}").unwrap(); + assert_eq!( + ast[0], + Node::VariableBlock(WS::default(), Expr::new_bitnot(ExprVal::Ident("id".to_string()))) + ); +} + #[test] fn parse_variable_tag_negated_expr_with_parentheses() { let ast = parse("{{ (not id or not true) and not 1 + 1 }}").unwrap(); diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index c7b97439..d828c4f1 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::convert::TryInto; use std::io::Write; use serde_json::{to_string_pretty, to_value, Number, Value}; @@ -630,11 +631,28 @@ impl<'a> Processor<'a> { res = self.eval_filter(&res, filter, &mut needs_escape, body_recursion_level)?; } - // Lastly, we need to check if the expression is negated, thus turning it into a bool + // We need to check if the expression is negated, thus turning it into a bool if expr.negated { return Ok(Cow::Owned(Value::Bool(!res.is_truthy()))); } + // Check for bitnot + if expr.bitnot { + match *res { + Value::Number(ref n) => { + if let Some(n) = n.as_i64() { + return Ok(Cow::Owned(Value::Number(Number::from(!n)))); + } + } + _ => { + return Err(Error::msg(format!( + "Tried to apply `bitnot` to a non-number value: {:?}", + res + ))); + } + } + } + // Checks if it's a string and we need to escape it (if the last filter is `safe` we don't) if self.should_escape && needs_escape && res.is_string() && !expr.is_marked_safe() { res = Cow::Owned( @@ -869,6 +887,12 @@ impl<'a> Processor<'a> { if bool_expr.negated { res = !res; } + + if bool_expr.bitnot { + return Err(Error::msg( + "Bitwise not (two's complement) operator `bitnot` can only be used on numbers in logic expressions", + )); + } res } ExprVal::Math(_) | ExprVal::Int(_) | ExprVal::Float(_) => { @@ -914,6 +938,12 @@ impl<'a> Processor<'a> { return Ok(!res); } + if bool_expr.bitnot { + return Err(Error::msg( + "Bitwise not (two's complement) operator `bitnot` can only be used on numbers in logic expressions", + )); + } + Ok(res) } @@ -1147,6 +1177,101 @@ impl<'a> Processor<'a> { Number::from_f64(ll % rr) } } + MathOperator::BitOr => { + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + Some(Number::from(ll | rr)) + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + Some(Number::from(ll | rr)) + } else { + return Err(Error::msg( + "The `|` operator can only be used on numbers in math expressions that can be cast to integers", + )); + } + } + MathOperator::BitXor => { + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + Some(Number::from(ll ^ rr)) + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + Some(Number::from(ll ^ rr)) + } else { + return Err(Error::msg( + "The `^` operator can only be used on numbers in math expressions that can be cast to integers", + )); + } + } + MathOperator::BitAnd => { + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + Some(Number::from(ll & rr)) + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + Some(Number::from(ll & rr)) + } else { + return Err(Error::msg( + "The `&` operator can only be used on numbers in math expressions that can be cast to integers", + )); + } + } + MathOperator::BitLeftShift => { + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + if rr < 0 { + return Err(Error::msg( + "The `<<` operator can only be used with a positive number as the right operand", + )); + } + + let rr = rr.try_into().map_err(|_| { + Error::msg("The `>>` operator can only be used with a positive number that fits in a u32 as the right operand") + })?; + + Some(Number::from(ll.rotate_left(rr))) // To avoid overflows, we actually rotate left instead of shifting + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + Some(Number::from(ll << rr)) + } else { + return Err(Error::msg( + "The `<<` operator can only be used on numbers in math expressions that can be cast to integers", + )); + } + } + MathOperator::BitRightShift => { + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + if rr < 0 { + return Err(Error::msg( + "The `>>` operator can only be used with a positive number as the right operand", + )); + } + + let rr = rr.try_into().map_err(|_| { + Error::msg("The `>>` operator can only be used with a positive number that fits in a u32 as the right operand") + })?; + + Some(Number::from(ll.rotate_right(rr))) // To avoid overflows, we actually rotate right instead of shifting + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + Some(Number::from(ll >> rr)) + } else { + return Err(Error::msg( + "The `>>` operator can only be used on numbers in math expressions that can be cast to integers", + )); + } + } } } ExprVal::FunctionCall(ref fn_call) => { diff --git a/src/tera.rs b/src/tera.rs index 9335fcf3..51348288 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -735,6 +735,7 @@ impl Tera { self.register_filter("as_str", common::as_str); self.register_filter("get", object::get); + self.register_filter("merge", object::merge); } fn register_tera_testers(&mut self) { From 525fe033c0c8c9142680be11de55b9f31210e982 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Wed, 24 Jul 2024 16:47:21 +0000 Subject: [PATCH 13/15] improvements to grammar including power function --- src/constraints.rs | 3 ++ src/parser/ast.rs | 3 ++ src/parser/mod.rs | 6 ++- src/parser/tera.pest | 3 +- src/parser/tests/errors.rs | 12 +++--- src/renderer/processor.rs | 81 +++++++++++++++++++++++++++++++++---- src/renderer/stack_frame.rs | 28 +++++++++++++ src/renderer/tests/basic.rs | 35 ++++++++++++++-- 8 files changed, 151 insertions(+), 20 deletions(-) diff --git a/src/constraints.rs b/src/constraints.rs index 15ebbb36..22547d62 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -5,6 +5,9 @@ pub const RENDER_BODY_RECURSION_LIMIT: usize = 15; /// Stack frame size limit pub const STACK_FRAME_MAX_ENTRIES: usize = 50; +/// STACK_FRAME_MAX_SIZE +pub const STACK_FRAME_MAX_SIZE: usize = 1024 * 1024 * 4; + /// eval_expression max array element size pub const EXPRESSION_MAX_ARRAY_LENGTH: usize = 100; diff --git a/src/parser/ast.rs b/src/parser/ast.rs index fe2a5cb0..06db8ff9 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -23,6 +23,8 @@ pub enum MathOperator { Div, /// % Modulo, + /// ** + Power, /// bitor (bitwise or) BitOr, /// bitxor (bitwise xor) @@ -46,6 +48,7 @@ impl fmt::Display for MathOperator { MathOperator::Mul => "*", MathOperator::Div => "/", MathOperator::Modulo => "%", + MathOperator::Power => "**", MathOperator::BitOr => "bitor", MathOperator::BitXor => "bitxor", MathOperator::BitAnd => "bitand", diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e16cf38d..44b12cc6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -32,7 +32,8 @@ lazy_static! { .op(Op::infix(Rule::op_bitor, Assoc::Left) | Op::infix(Rule::op_bitxor, Assoc::Left) | Op::infix(Rule::op_bitand, Assoc::Left) | Op::infix(Rule::op_bitlshift, Assoc::Left) | Op::infix(Rule::op_bitrshift, Assoc::Left)) // bitor, bitxor, bitand, bitlshift, bitrshift .op(Op::infix(Rule::op_times, Assoc::Left) | Op::infix(Rule::op_slash, Assoc::Left) - | Op::infix(Rule::op_modulo, Assoc::Left)); // *, /, % + | Op::infix(Rule::op_modulo, Assoc::Left) + | Op::infix(Rule::op_power, Assoc::Left)); // *, /, %, ** static ref COMPARISON_EXPR_PARSER: PrattParser = PrattParser::new() .op(Op::infix(Rule::op_lt, Assoc::Left) | Op::infix(Rule::op_lte, Assoc::Left) | Op::infix(Rule::op_gt, Assoc::Left) @@ -264,6 +265,7 @@ fn parse_basic_expression(pair: Pair) -> TeraResult { Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, + Rule::op_power => MathOperator::Power, Rule::op_bitor => MathOperator::BitOr, Rule::op_bitxor => MathOperator::BitXor, Rule::op_bitand => MathOperator::BitAnd, @@ -443,6 +445,7 @@ fn parse_comparison_val(pair: Pair) -> TeraResult { Rule::op_times => MathOperator::Mul, Rule::op_slash => MathOperator::Div, Rule::op_modulo => MathOperator::Modulo, + Rule::op_power => MathOperator::Power, Rule::op_bitor => MathOperator::BitOr, Rule::op_bitxor => MathOperator::BitXor, Rule::op_bitand => MathOperator::BitAnd, @@ -1345,6 +1348,7 @@ pub fn parse(input: &str) -> TeraResult> { Rule::op_times => "`*`".to_string(), Rule::op_slash => "`/`".to_string(), Rule::op_modulo => "`%`".to_string(), + Rule::op_power => "`**`".to_string(), Rule::op_bitor => "`bitor`".to_string(), Rule::op_bitxor => "`bitxor`".to_string(), Rule::op_bitand => "`bitand`".to_string(), diff --git a/src/parser/tera.pest b/src/parser/tera.pest index 2d754791..8aad3935 100644 --- a/src/parser/tera.pest +++ b/src/parser/tera.pest @@ -47,6 +47,7 @@ op_minus = { "-" } op_times = { "*" } op_slash = { "/" } op_modulo = { "%" } +op_power = { "**" } op_bitor = { "bitor" } // bitwise or cannot use | op_bitxor = { "bitxor" } // bitwise xor cannot use ^ op_bitand = { "bitand" } // bitwise and cannot use & @@ -91,7 +92,7 @@ string_concat = { (fn_call | float | int | string | dotted_square_bracket_ident) // boolean first so they are not caught as identifiers basic_val = _{ boolean | test_not | test | macro_call | fn_call | dotted_square_bracket_ident | float | int } -basic_op = _{ op_plus | op_minus | op_times | op_slash | op_modulo | op_bitor | op_bitxor | op_bitand | op_bitlshift | op_bitrshift } +basic_op = _{ op_plus | op_minus | op_times | op_slash | op_modulo | op_power | op_bitor | op_bitxor | op_bitand | op_bitlshift | op_bitrshift } basic_expr = { ("(" ~ basic_expr ~ ")" | basic_val) ~ (basic_op ~ ("(" ~ basic_expr ~ ")" | basic_val))* } basic_expr_filter = !{ basic_expr ~ filter* } string_expr_filter = !{ (string_concat | string) ~ filter* } diff --git a/src/parser/tests/errors.rs b/src/parser/tests/errors.rs index 8b58c440..879a4b08 100644 --- a/src/parser/tests/errors.rs +++ b/src/parser/tests/errors.rs @@ -19,7 +19,7 @@ fn invalid_number() { "{{ 1.2.2 }}", &[ "1:7", - "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" + "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, `**`, `bitor`, `bitxor`, `bitand`, `bitlshift`, `bitrshift`, a filter, or a variable end (`}}`)" ], ); } @@ -35,7 +35,7 @@ fn wrong_start_block() { "{{ if true %}", &[ "1:7", - "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" + "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, `**`, `bitor`, `bitxor`, `bitand`, `bitlshift`, `bitrshift`, a filter, or a variable end (`}}`)" ], ); } @@ -57,7 +57,7 @@ fn unterminated_variable_block() { "{{ hey", &[ "1:7", - "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" + "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, `**`, `bitor`, `bitxor`, `bitand`, `bitlshift`, `bitrshift`, a filter, or a variable end (`}}`)" ], ); } @@ -168,7 +168,7 @@ fn invalid_operator() { "{{ hey =! }}", &[ "1:8", - "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" + "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, `**`, `bitor`, `bitxor`, `bitand`, `bitlshift`, `bitrshift`, a filter, or a variable end (`}}`)" ], ); } @@ -225,7 +225,7 @@ fn invalid_macro_call() { "{{ my:macro() }}", &[ "1:6", - "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, a filter, or a variable end (`}}`)" + "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, `**`, `bitor`, `bitxor`, `bitand`, `bitlshift`, `bitrshift`, a filter, or a variable end (`}}`)" ], ); } @@ -282,7 +282,7 @@ fn invalid_test_argument() { r#"{% if a is odd(key=1) %}"#, &[ "1:19", - "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, or a filter" + "expected `or`, `and`, `not`, `<=`, `>=`, `<`, `>`, `==`, `!=`, `+`, `-`, `*`, `/`, `%`, `**`, `bitor`, `bitxor`, `bitand`, `bitlshift`, `bitrshift`, or a filter" ], ); } diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index d828c4f1..5b155401 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -1029,8 +1029,14 @@ impl<'a> Processor<'a> { }; Some(Number::from(res)) } else { - let ll = l.as_f64().unwrap(); - let rr = r.as_f64().unwrap(); + let ll = l.as_f64().ok_or(Error::msg(format!( + "Tried to multiply a number with an unsupported type: {:?}", + l + )))?; + let rr = r.as_f64().ok_or(Error::msg(format!( + "Tried to multiply a number with an unsupported type: {:?}", + r + )))?; Number::from_f64(ll * rr) } @@ -1063,8 +1069,14 @@ impl<'a> Processor<'a> { } } } else { - let ll = l.as_f64().unwrap(); - let rr = r.as_f64().unwrap(); + let ll = l.as_f64().ok_or(Error::msg(format!( + "Tried to divide a number with an unsupported type: {:?}", + l + )))?; + let rr = r.as_f64().ok_or(Error::msg(format!( + "Tried to divide a number with an unsupported type: {:?}", + r + )))?; if rr == 0.0 { return Err(Error::msg(format!( @@ -1112,8 +1124,12 @@ impl<'a> Processor<'a> { }; Some(Number::from(res)) } else { - let ll = l.as_f64().unwrap(); - let rr = r.as_f64().unwrap(); + let ll = l.as_f64().ok_or(Error::msg( + "The `+` operator can only be used on numbers in math expressions", + ))?; + let rr = r.as_f64().ok_or(Error::msg( + "The `+` operator can only be used on numbers in math expressions", + ))?; Some(Number::from_f64(ll + rr).unwrap()) } } @@ -1172,11 +1188,60 @@ impl<'a> Processor<'a> { } Some(Number::from(ll % rr)) } else { - let ll = l.as_f64().unwrap(); - let rr = r.as_f64().unwrap(); + let ll = l.as_f64().ok_or(Error::msg( + "The `%` operator can only be used on numbers in math expressions", + ))?; + let rr = r.as_f64().ok_or(Error::msg( + "The `%` operator can only be used on numbers in math expressions", + ))?; Number::from_f64(ll % rr) } } + MathOperator::Power => { + if l.is_i64() && r.is_i64() { + let ll = l.as_i64().unwrap(); + let rr = r.as_i64().unwrap(); + if rr < 0 { + return Err(Error::msg( + "The `**` operator can only be used with a positive number as the right operand", + )); + } + + let rr = rr.try_into().map_err(|_| { + Error::msg("The `**` operator can only be used with a positive number that fits in a u32 as the right operand") + })?; + + let res = ll.checked_pow(rr).ok_or(Error::msg(format!( + "{} ** {} results in an out of bounds i64", + ll, rr + )))?; + + Some(Number::from(res)) + } else if l.is_u64() && r.is_u64() { + let ll = l.as_u64().unwrap(); + let rr = r.as_u64().unwrap(); + + let rr = rr.try_into().map_err(|_| { + Error::msg("The `**` operator can only be used with a positive number that fits in a u32 as the right operand") + })?; + + let res = ll.checked_pow(rr).ok_or(Error::msg(format!( + "{} ** {} results in an out of bounds i64", + ll, rr + )))?; + + Some(Number::from(res)) + } else { + let ll = l.as_f64().ok_or(Error::msg( + "The `**` operator can only be used on numbers in math expressions", + ))?; + let rr = r.as_f64().ok_or(Error::msg( + "The `**` operator can only be used on numbers in math expressions", + ))?; + + Number::from_f64(ll.powf(rr)) + } + } MathOperator::BitOr => { if l.is_i64() && r.is_i64() { let ll = l.as_i64().unwrap(); diff --git a/src/renderer/stack_frame.rs b/src/renderer/stack_frame.rs index cf877d73..006fd326 100644 --- a/src/renderer/stack_frame.rs +++ b/src/renderer/stack_frame.rs @@ -52,6 +52,8 @@ pub struct StackFrame<'a> { pub for_loop: Option>, /// Macro namespace if MacroFrame pub macro_namespace: Option<&'a str>, + /// Size of the frame + pub size: usize, } impl<'a> StackFrame<'a> { @@ -63,6 +65,7 @@ impl<'a> StackFrame<'a> { active_template: tpl, for_loop: None, macro_namespace: None, + size: 0, } } @@ -74,6 +77,7 @@ impl<'a> StackFrame<'a> { active_template: tpl, for_loop: Some(for_loop), macro_namespace: None, + size: 0, } } @@ -90,6 +94,7 @@ impl<'a> StackFrame<'a> { active_template: tpl, for_loop: None, macro_namespace: Some(macro_namespace), + size: 0, } } @@ -101,6 +106,7 @@ impl<'a> StackFrame<'a> { active_template: tpl, for_loop: None, macro_namespace: None, + size: 0, } } @@ -182,7 +188,29 @@ impl<'a> StackFrame<'a> { if self.context.len() >= crate::constraints::STACK_FRAME_MAX_ENTRIES { return Err(Error::msg("Stack frame context is full")); } + + if self.size >= crate::constraints::STACK_FRAME_MAX_SIZE { + return Err(Error::msg("Stack frame context is too big")); + } + self.context.insert(key, value); + + // Get size of self.context + self.size = self.context.iter().fold(0, |acc, (k, v)| { + acc + k.len() + + match v { + Cow::Borrowed(v) => v.to_string().len(), + Cow::Owned(v) => v.to_string().len(), + } + }); + + // Check one more time now with the new size + if self.size >= crate::constraints::STACK_FRAME_MAX_SIZE { + // Remove element + self.context.remove(key); + return Err(Error::msg("Stack frame context is too big")); + } + Ok(()) } diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs index f2cdf8bb..e51ada13 100644 --- a/src/renderer/tests/basic.rs +++ b/src/renderer/tests/basic.rs @@ -56,7 +56,6 @@ fn render_variable_block_lit_expr() { ("{{ (2 + 1) * 2 }}", "6"), ("{{ 2 * 4 % 8 }}", "0"), ("{{ 2.8 * 2 | round }}", "6"), - ("{{ 1 / 0 }}", "NaN"), ("{{ true and 10 }}", "true"), ("{{ true and not 10 }}", "false"), ("{{ not true }}", "false"), @@ -349,7 +348,6 @@ fn add_set_values_in_context() { ("{% set i = range(end=3) %}{{ i }}", "[0, 1, 2]"), ("{% set i = admin or true %}{{ i }}", "true"), ("{% set i = admin and num > 0 %}{{ i }}", "true"), - ("{% set i = 0 / 0 %}{{ i }}", "NaN"), ("{% set i = [1,2] %}{{ i }}", "[1, 2]"), ]; @@ -452,8 +450,6 @@ fn render_if_elif_else() { ("{% if not undefined %}a{% endif %}", "a"), ("{% if not is_false and is_true %}a{% endif %}", "a"), ("{% if not is_false or numbers | length > 0 %}a{% endif %}", "a"), - // doesn't panic with NaN results - ("{% if 0 / 0 %}a{% endif %}", ""), // if and else ("{% if is_true %}Admin{% else %}User{% endif %}", "Admin"), ("{% if is_false %}Admin{% else %}User{% endif %}", "User"), @@ -990,3 +986,34 @@ fn safe_function_works() { let res = tera.render("test.html", &Context::new()); assert_eq!(res.unwrap(), "

Hello
"); } + +#[test] +fn test_pemdas() { + let mut tera = Tera::default(); + let ctx = Context::new(); + let r = tera.render_str("{{ (1 + 2) * (3 + 4) }}", &ctx); + + assert_eq!(r.unwrap(), "21"); + + let r = tera.render_str("{{ 2 * (1 + 2) }}", &ctx); + + assert_eq!(r.unwrap(), "6"); + + let r = tera.render_str("{{ 2 * (1 << 2) }}", &ctx); + + assert_eq!(r.unwrap(), "8"); + + let r = tera.render_str("{{ 2 * (1 >> 0) }}", &ctx); + + assert_eq!(r.unwrap(), "2"); +} + +#[test] +fn test_zero_div_zero() { + let mut tera = Tera::default(); + let ctx = Context::new(); + let r = tera.render_str("{{ 1 / 0 }}", &ctx); + + let err = format!("{:?}", r.err().unwrap()); + assert!(err.contains("division by zero")); +} From 87a409ebe7fa6e7ed922e422860c86cf6a96a297 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Thu, 25 Jul 2024 10:44:51 +0000 Subject: [PATCH 14/15] add macros to async --- Cargo.toml | 3 +- src/constraints.rs | 2 +- src/renderer/processor.rs | 309 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 299 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 82614f06..c4fddf57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ chrono-tz = {version = "0.9", optional = true} rand = {version = "0.8", optional = true} # used in async +tokio = {version = "1", optional = true} async-recursion = {version = "1", optional = true} [dev-dependencies] @@ -48,7 +49,7 @@ tempfile = "3" [features] default = ["builtins", "async"] -async = ["dep:async-recursion"] +async = ["dep:async-recursion", "dep:tokio"] builtins = ["urlencode", "slug", "humansize", "chrono", "chrono-tz", "rand"] urlencode = ["percent-encoding"] preserve_order = ["serde_json/preserve_order"] diff --git a/src/constraints.rs b/src/constraints.rs index 22547d62..a1f7740a 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -1,6 +1,6 @@ /// Renderer limits pub const RENDER_BLOCK_MAX_DEPTH: usize = 5; -pub const RENDER_BODY_RECURSION_LIMIT: usize = 15; +pub const RENDER_BODY_MAX_DEPTH: usize = 20; /// Stack frame size limit pub const STACK_FRAME_MAX_ENTRIES: usize = 50; diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index 5b155401..a0d3f476 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -157,10 +157,11 @@ impl<'a> Processor<'a> { write: &mut impl Write, body_recursion_level: usize, ) -> Result<()> { - if body_recursion_level >= crate::constraints::RENDER_BODY_RECURSION_LIMIT { + if body_recursion_level >= crate::constraints::RENDER_BODY_MAX_DEPTH { return Err(Error::msg(format!( - "Max recursion limit reached while rendering body ({} levels)", - crate::constraints::RENDER_BODY_RECURSION_LIMIT + "Max body depth reached while rendering body ({} > max of {})", + body_recursion_level, + crate::constraints::RENDER_BODY_MAX_DEPTH ))); } @@ -183,10 +184,11 @@ impl<'a> Processor<'a> { write: &mut (impl Write + Send + Sync), body_recursion_level: usize, ) -> Result<()> { - if body_recursion_level >= crate::constraints::RENDER_BODY_RECURSION_LIMIT { + if body_recursion_level >= crate::constraints::RENDER_BODY_MAX_DEPTH { return Err(Error::msg(format!( - "Max recursion limit reached while rendering body ({} levels)", - crate::constraints::RENDER_BODY_RECURSION_LIMIT + "Max body depth reached while rendering body ({} > max of {})", + body_recursion_level, + crate::constraints::RENDER_BODY_MAX_DEPTH ))); } @@ -206,7 +208,7 @@ impl<'a> Processor<'a> { fn create_for_loop( &mut self, for_loop: &'a Forloop, - body_recursion_level: usize, + container_val: Cow<'a, Value>, ) -> Result> { let container_name = match for_loop.container.val { ExprVal::Ident(ref ident) => ident, @@ -218,8 +220,6 @@ impl<'a> Processor<'a> { ))), }; - let container_val = self.safe_eval_expression(&for_loop.container, body_recursion_level)?; - let for_loop = match *container_val { Value::Array(_) => { if for_loop.key.is_some() { @@ -278,7 +278,8 @@ impl<'a> Processor<'a> { let for_loop_body = &for_loop.body; let for_loop_empty_body = &for_loop.empty_body; - let for_loop = self.create_for_loop(for_loop, body_recursion_level)?; + let container_val = self.safe_eval_expression(&for_loop.container, body_recursion_level)?; + let for_loop = self.create_for_loop(for_loop, container_val)?; let len = for_loop.len(); match (len, for_loop_empty_body) { @@ -315,7 +316,9 @@ impl<'a> Processor<'a> { let for_loop_body = &for_loop.body; let for_loop_empty_body = &for_loop.empty_body; - let for_loop = self.create_for_loop(for_loop, body_recursion_level)?; + let container_val = + self.safe_eval_expression_async(&for_loop.container, body_recursion_level).await?; + let for_loop = self.create_for_loop(for_loop, container_val)?; let len = for_loop.len(); match (len, for_loop_empty_body) { @@ -663,6 +666,176 @@ impl<'a> Processor<'a> { Ok(res) } + #[cfg(feature = "async")] + #[async_recursion::async_recursion] + async fn eval_expression_async( + &mut self, + expr: &'a Expr, + body_recursion_level: usize, + ) -> Result> { + let mut needs_escape = false; + + let mut res = match expr.val { + ExprVal::Array(ref arr) => { + if arr.len() > crate::constraints::EXPRESSION_MAX_ARRAY_LENGTH { + return Err(Error::msg(format!( + "Max number of elements in an array literal is {}, {:?}", + crate::constraints::EXPRESSION_MAX_ARRAY_LENGTH, + expr.val + ))); + } + + let mut values = vec![]; + for v in arr { + values.push( + self.eval_expression_async(v, body_recursion_level).await?.into_owned(), + ); + } + Cow::Owned(Value::Array(values)) + } + ExprVal::In(ref in_cond) => { + Cow::Owned(Value::Bool(self.eval_in_condition(in_cond, body_recursion_level)?)) + } + ExprVal::String(ref val) => { + needs_escape = true; + Cow::Owned(Value::String(val.to_string())) + } + ExprVal::StringConcat(ref str_concat) => { + let mut res = String::new(); + for s in &str_concat.values { + match *s { + ExprVal::String(ref v) => res.push_str(v), + ExprVal::Int(ref v) => res.push_str(&format!("{}", v)), + ExprVal::Float(ref v) => res.push_str(&format!("{}", v)), + ExprVal::Ident(ref i) => match *self.lookup_ident(i)? { + Value::String(ref v) => res.push_str(v), + Value::Number(ref v) => res.push_str(&v.to_string()), + _ => return Err(Error::msg(format!( + "Tried to concat a value that is not a string or a number from ident {}", + i + ))), + }, + ExprVal::FunctionCall(ref fn_call) => match *self.eval_tera_fn_call_async(fn_call, &mut needs_escape, body_recursion_level).await? { + Value::String(ref v) => res.push_str(v), + Value::Number(ref v) => res.push_str(&v.to_string()), + _ => return Err(Error::msg(format!( + "Tried to concat a value that is not a string or a number from function call {}", + fn_call.name + ))), + }, + _ => return Err(Error::msg(format!("Unimplemented expression found in line {:?} [{:?}]", s, expr.val))), + }; + } + + Cow::Owned(Value::String(res)) + } + ExprVal::Int(val) => Cow::Owned(Value::Number(val.into())), + ExprVal::Float(val) => Cow::Owned(Value::Number(Number::from_f64(val).unwrap())), + ExprVal::Bool(val) => Cow::Owned(Value::Bool(val)), + ExprVal::Ident(ref ident) => { + needs_escape = ident != MAGICAL_DUMP_VAR; + // Negated idents are special cased as `not undefined_ident` should not + // error but instead be falsy values + match self.lookup_ident(ident) { + Ok(val) => { + if val.is_null() && expr.has_default_filter() { + self.get_default_value(expr, body_recursion_level)? + } else { + val + } + } + Err(e) => { + if expr.has_default_filter() { + self.get_default_value(expr, body_recursion_level)? + } else { + if !expr.negated { + return Err(e); + } + // A negative undefined ident is !false so truthy + return Ok(Cow::Owned(Value::Bool(true))); + } + } + } + } + ExprVal::FunctionCall(ref fn_call) => { + self.eval_tera_fn_call_async(fn_call, &mut needs_escape, body_recursion_level) + .await? + } + ExprVal::MacroCall(ref macro_call) => { + // Render to string doesnt support async yet so just do it ourselves + /* + pub(crate) fn render_to_string(context: C, render: F) -> Result + where + C: FnOnce() -> String, + F: FnOnce(&mut Vec) -> Result<(), E>, + Error: From, + { + let mut buffer = Vec::new(); + render(&mut buffer).map_err(Error::from)?; + buffer_to_string(context, buffer) + } + */ + + let mut buffer = Vec::new(); + self.eval_macro_call_async(macro_call, &mut buffer, body_recursion_level).await?; + let body = + crate::utils::buffer_to_string(|| format!("macro {}", macro_call.name), buffer) + .map_err(Error::from)?; + + Cow::Owned(Value::String(body)) + } + ExprVal::Test(ref test) => { + Cow::Owned(Value::Bool(self.eval_test(test, body_recursion_level)?)) + } + ExprVal::Logic(_) => { + Cow::Owned(Value::Bool(self.eval_as_bool(expr, body_recursion_level)?)) + } + ExprVal::Math(_) => match self.eval_as_number(&expr.val, body_recursion_level) { + Ok(Some(n)) => Cow::Owned(Value::Number(n)), + Ok(None) => Cow::Owned(Value::String("NaN".to_owned())), + Err(e) => return Err(Error::msg(e)), + }, + }; + + for filter in &expr.filters { + if filter.name == "safe" || filter.name == "default" { + continue; + } + res = self.eval_filter(&res, filter, &mut needs_escape, body_recursion_level)?; + } + + // We need to check if the expression is negated, thus turning it into a bool + if expr.negated { + return Ok(Cow::Owned(Value::Bool(!res.is_truthy()))); + } + + // Check for bitnot + if expr.bitnot { + match *res { + Value::Number(ref n) => { + if let Some(n) = n.as_i64() { + return Ok(Cow::Owned(Value::Number(Number::from(!n)))); + } + } + _ => { + return Err(Error::msg(format!( + "Tried to apply `bitnot` to a non-number value: {:?}", + res + ))); + } + } + } + + // Checks if it's a string and we need to escape it (if the last filter is `safe` we don't) + if self.should_escape && needs_escape && res.is_string() && !expr.is_marked_safe() { + res = Cow::Owned( + to_value(self.tera.get_escape_fn()(res.as_str().unwrap())).map_err(Error::json)?, + ); + } + + Ok(res) + } + /// Render an expression and never escape its result fn safe_eval_expression( &mut self, @@ -676,6 +849,22 @@ impl<'a> Processor<'a> { res } + /// Render an expression and never escape its result + /// + /// This is the async version of `safe_eval_expression` + #[cfg(feature = "async")] + async fn safe_eval_expression_async( + &mut self, + expr: &'a Expr, + body_recursion_level: usize, + ) -> Result> { + let should_escape = self.should_escape; + self.should_escape = false; + let res = self.eval_expression_async(expr, body_recursion_level).await; + self.should_escape = should_escape; + res + } + /// Evaluate a set tag and add the value to the right context fn eval_set(&mut self, set: &'a Set, body_recursion_level: usize) -> Result<()> { let assigned_value = self.safe_eval_expression(&set.value, body_recursion_level)?; @@ -683,6 +872,17 @@ impl<'a> Processor<'a> { Ok(()) } + #[cfg(feature = "async")] + /// Evaluate a set tag and add the value to the right context + /// + /// This is the async version of `eval_set` + async fn eval_set_async(&mut self, set: &'a Set, body_recursion_level: usize) -> Result<()> { + let assigned_value = + self.safe_eval_expression_async(&set.value, body_recursion_level).await?; + self.call_stack.add_assignment(&set.key[..], set.global, assigned_value)?; + Ok(()) + } + fn eval_test(&mut self, test: &'a Test, body_recursion_level: usize) -> Result { let tester_fn = self.tera.get_tester(&test.name)?; let err_wrap = |e| Error::call_test(&test.name, e); @@ -732,6 +932,33 @@ impl<'a> Processor<'a> { Ok(Cow::Owned(tera_fn.call(&args).map_err(err_wrap)?)) } + #[cfg(feature = "async")] + async fn eval_tera_fn_call_async( + &mut self, + function_call: &'a FunctionCall, + needs_escape: &mut bool, + body_recursion_level: usize, + ) -> Result> { + let tera_fn = self.tera.get_function(&function_call.name)?; + *needs_escape = !tera_fn.is_safe(); + + let err_wrap = |e| Error::call_function(&function_call.name, e); + + let mut args = HashMap::with_capacity(function_call.args.len()); + for (arg_name, expr) in &function_call.args { + args.insert( + arg_name.to_string(), + self.safe_eval_expression_async(expr, body_recursion_level) + .await + .map_err(err_wrap)? + .clone() + .into_owned(), + ); + } + + Ok(Cow::Owned(tera_fn.call(&args).map_err(err_wrap)?)) + } + fn eval_macro_call( &mut self, macro_call: &'a MacroCall, @@ -785,6 +1012,62 @@ impl<'a> Processor<'a> { Ok(()) } + #[cfg(feature = "async")] + async fn eval_macro_call_async( + &mut self, + macro_call: &'a MacroCall, + write: &mut (impl Write + Send + Sync), + body_recursion_level: usize, + ) -> Result<()> { + let active_template_name = if let Some(block) = self.blocks.last() { + block.1 + } else if self.template.name != self.template_root.name { + &self.template_root.name + } else { + &self.call_stack.active_template().name + }; + + let (macro_template_name, macro_definition) = self.macros.lookup_macro( + active_template_name, + ¯o_call.namespace[..], + ¯o_call.name[..], + )?; + + let mut frame_context = FrameContext::with_capacity(macro_definition.args.len()); + + // First the default arguments + for (arg_name, default_value) in ¯o_definition.args { + let value = match macro_call.args.get(arg_name) { + Some(val) => self.safe_eval_expression_async(val, body_recursion_level).await?, + None => match *default_value { + Some(ref val) => { + self.safe_eval_expression_async(val, body_recursion_level).await? + } + None => { + return Err(Error::msg(format!( + "Macro `{}` is missing the argument `{}`", + macro_call.name, arg_name + ))); + } + }, + }; + frame_context.insert(arg_name, value); + } + + self.call_stack.push_macro_frame( + ¯o_call.namespace, + ¯o_call.name, + frame_context, + self.tera.get_template(macro_template_name)?, + ); + + self.render_body_async(¯o_definition.body, write, body_recursion_level).await?; + + self.call_stack.pop(); + + Ok(()) + } + fn eval_filter( &mut self, value: &Val<'a>, @@ -1572,9 +1855,9 @@ impl<'a> Processor<'a> { Node::Comment(_, _) => (), Node::Text(ref s) | Node::Raw(_, ref s, _) => write!(write, "{}", s)?, Node::VariableBlock(_, ref expr) => { - self.eval_expression(expr, body_recursion_level)?.render(write)? + self.eval_expression_async(expr, body_recursion_level).await?.render(write)? } - Node::Set(_, ref set) => self.eval_set(set, body_recursion_level)?, + Node::Set(_, ref set) => self.eval_set_async(set, body_recursion_level).await?, Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { // Render to string doesnt support async yet so just do it ourselves /* From c9b8ce55dcb54c094d21df37603cb7651fa833d8 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Thu, 25 Jul 2024 12:46:51 +0000 Subject: [PATCH 15/15] feat: add delete and delete_global to templates --- src/parser/ast.rs | 13 +++++++++++++ src/parser/mod.rs | 29 +++++++++++++++++++++++++++++ src/parser/tera.pest | 17 +++++++++++++++++ src/parser/tests/parser.rs | 18 ++++++++++++++++++ src/parser/whitespace.rs | 1 + src/renderer/call_stack.rs | 11 +++++++++++ src/renderer/processor.rs | 13 +++++++++++++ src/renderer/stack_frame.rs | 4 ++++ src/renderer/tests/basic.rs | 7 +++++++ 9 files changed, 113 insertions(+) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 06db8ff9..bb85c533 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -272,6 +272,16 @@ pub struct Set { pub global: bool, } +/// Set a variable in the context `{% delete val %}` +#[derive(Clone, Debug, PartialEq)] +pub struct Delete { + /// The name for that value in the context + pub key: String, + /// Whether we want to delete the variable globally or locally + /// global_delete is only useful in loops + pub global: bool, +} + /// A call to a namespaced macro `macros::my_macro()` #[derive(Clone, Debug, PartialEq)] pub struct MacroCall { @@ -349,6 +359,9 @@ pub enum Node { /// The `{% set val = something %}` tag Set(WS, Set), + /// The `{% delete val %}` tag + Delete(WS, Delete), + /// The text between `{% raw %}` and `{% endraw %}` Raw(WS, String, WS), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44b12cc6..3d8b4d57 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -764,6 +764,31 @@ fn parse_set_tag(pair: Pair, global: bool) -> TeraResult { Ok(Node::Set(ws, Set { key: key.unwrap(), value: expr.unwrap(), global })) } +fn parse_delete_tag(pair: Pair, global: bool) -> TeraResult { + let mut ws = WS::default(); + let mut key = None; + + for p in pair.into_inner() { + match p.as_rule() { + Rule::tag_start => { + ws.left = p.as_span().as_str() == "{%-"; + } + Rule::tag_end => { + ws.right = p.as_span().as_str() == "-%}"; + } + Rule::ident => key = Some(p.as_str().to_string()), + _ => { + return Err(Error::msg(format!( + "PARSER ERROR: Unsupported rule {:?} in parse_delete_tag", + p.as_rule() + ))) + } + } + } + + Ok(Node::Delete(ws, Delete { key: key.unwrap(), global })) +} + fn parse_raw_tag(pair: Pair) -> Node { let mut start_ws = WS::default(); let mut end_ws = WS::default(); @@ -1271,6 +1296,8 @@ fn parse_content(pair: Pair) -> TeraResult> { Rule::super_tag => nodes.push(Node::Super), Rule::set_tag => nodes.push(parse_set_tag(p, false)?), Rule::set_global_tag => nodes.push(parse_set_tag(p, true)?), + Rule::delete_tag => nodes.push(parse_delete_tag(p, false)?), + Rule::delete_global_tag => nodes.push(parse_delete_tag(p, true)?), Rule::raw => nodes.push(parse_raw_tag(p)), Rule::variable_tag => nodes.push(parse_variable_tag(p)?), Rule::forloop => nodes.push(parse_forloop(p)?), @@ -1373,6 +1400,8 @@ pub fn parse(input: &str) -> TeraResult> { Rule::filter_section_content => "the filter section content".to_string(), Rule::set_tag => "a `set` tag`".to_string(), Rule::set_global_tag => "a `set_global` tag`".to_string(), + Rule::delete_tag => "a `delete` tag`".to_string(), + Rule::delete_global_tag => "a `delete_global` tag`".to_string(), Rule::block_content | Rule::content | Rule::for_content => { "some content".to_string() }, diff --git a/src/parser/tera.pest b/src/parser/tera.pest index 8aad3935..e489e7ce 100644 --- a/src/parser/tera.pest +++ b/src/parser/tera.pest @@ -196,6 +196,15 @@ set_global_tag = ${ ~ "set_global" ~ WHITESPACE+ ~ ident ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ (logic_expr | array_filter) ~ WHITESPACE* ~ tag_end } +delete_tag = ${ + tag_start ~ WHITESPACE* + ~ "delete" ~ WHITESPACE+ ~ ident + ~ WHITESPACE* ~ tag_end +} +delete_global_tag = ${ + tag_start ~ WHITESPACE* + ~ "delete_global" ~ WHITESPACE+ ~ ident ~ WHITESPACE* ~ tag_end +} endblock_tag = !{ tag_start ~ "endblock" ~ ident? ~ tag_end } endmacro_tag = !{ tag_start ~ "endmacro" ~ ident? ~ tag_end } endif_tag = !{ tag_start ~ "endif" ~ tag_end } @@ -248,6 +257,8 @@ macro_content = @{ comment_tag | set_tag | set_global_tag | + delete_tag | + delete_global_tag | macro_if | forloop | filter_section | @@ -263,6 +274,8 @@ block_content = @{ comment_tag | set_tag | set_global_tag | + delete_tag | + delete_global_tag | block | block_if | forloop | @@ -278,6 +291,8 @@ for_content = @{ comment_tag | set_tag | set_global_tag | + delete_tag | + delete_global_tag | for_if | forloop | break_tag | @@ -293,6 +308,8 @@ content = @{ comment_tag | set_tag | set_global_tag | + delete_tag | + delete_global_tag | block | content_if | forloop | diff --git a/src/parser/tests/parser.rs b/src/parser/tests/parser.rs index f0c0b193..8ae76ab2 100644 --- a/src/parser/tests/parser.rs +++ b/src/parser/tests/parser.rs @@ -840,6 +840,24 @@ fn parse_set_global_tag() { ); } +#[test] +fn parse_delete() { + let ast = parse("{% delete hello %}").unwrap(); + assert_eq!( + ast[0], + Node::Delete(WS::default(), Delete { key: "hello".to_string(), global: false },) + ); +} + +#[test] +fn parse_delete_global() { + let ast = parse("{% delete_global hello %}").unwrap(); + assert_eq!( + ast[0], + Node::Delete(WS::default(), Delete { key: "hello".to_string(), global: true },) + ); +} + #[test] fn parse_raw_tag() { let ast = parse("{% raw -%}{{hey}}{%- endraw %}").unwrap(); diff --git a/src/parser/whitespace.rs b/src/parser/whitespace.rs index 5af5c945..0d399902 100644 --- a/src/parser/whitespace.rs +++ b/src/parser/whitespace.rs @@ -58,6 +58,7 @@ pub fn remove_whitespace(nodes: Vec, body_ws: Option) -> Vec { | Node::Extends(ws, _) | Node::Include(ws, _, _) | Node::Set(ws, _) + | Node::Delete(ws, _) | Node::Break(ws) | Node::Comment(ws, _) | Node::Continue(ws) => { diff --git a/src/renderer/call_stack.rs b/src/renderer/call_stack.rs index 181b0fdd..67b7fbb5 100644 --- a/src/renderer/call_stack.rs +++ b/src/renderer/call_stack.rs @@ -137,6 +137,17 @@ impl<'a> CallStack<'a> { Ok(()) } + /// Deletes an assignment value (via {% delete ... %} and {% delete_global ... %} ) + /// + /// This returns the deleted value + pub fn delete_assignment(&mut self, key: &'a str, global: bool) -> Result> { + if global { + self.global_frame_mut().remove(key) + } else { + self.current_frame_mut().remove(key) + } + } + /// Breaks current for loop pub fn break_for_loop(&mut self) -> Result<()> { match self.current_frame_mut().for_loop { diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs index a0d3f476..ad2f41f4 100644 --- a/src/renderer/processor.rs +++ b/src/renderer/processor.rs @@ -883,6 +883,13 @@ impl<'a> Processor<'a> { Ok(()) } + /// Evaluate a delete tag and remove the value from the right context + /// + /// Unlike set, there is no async mode for delete as delete just removes a value from the context + fn eval_delete(&mut self, delete: &'a Delete) -> Result> { + self.call_stack.delete_assignment(&delete.key[..], delete.global) + } + fn eval_test(&mut self, test: &'a Test, body_recursion_level: usize) -> Result { let tester_fn = self.tera.get_tester(&test.name)?; let err_wrap = |e| Error::call_test(&test.name, e); @@ -1771,6 +1778,9 @@ impl<'a> Processor<'a> { self.eval_expression(expr, body_recursion_level)?.render(write)? } Node::Set(_, ref set) => self.eval_set(set, body_recursion_level)?, + Node::Delete(_, ref del) => { + self.eval_delete(del)?; // TODO: What should happen to the existing value? + } Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { let body = render_to_string( || format!("filter {}", filter.name), @@ -1858,6 +1868,9 @@ impl<'a> Processor<'a> { self.eval_expression_async(expr, body_recursion_level).await?.render(write)? } Node::Set(_, ref set) => self.eval_set_async(set, body_recursion_level).await?, + Node::Delete(_, ref del) => { + self.eval_delete(del)?; // TODO: What should happen to the existing value? + } Node::FilterSection(_, FilterSection { ref filter, ref body }, _) => { // Render to string doesnt support async yet so just do it ourselves /* diff --git a/src/renderer/stack_frame.rs b/src/renderer/stack_frame.rs index 006fd326..24f42b63 100644 --- a/src/renderer/stack_frame.rs +++ b/src/renderer/stack_frame.rs @@ -214,6 +214,10 @@ impl<'a> StackFrame<'a> { Ok(()) } + pub fn remove(&mut self, key: &str) -> Result> { + self.context.remove(key).ok_or_else(|| Error::msg(format!("Key {} not found", key))) + } + /// Context is cleared on each loop pub fn clear_context(&mut self) { if self.for_loop.is_some() { diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs index e51ada13..84e3af95 100644 --- a/src/renderer/tests/basic.rs +++ b/src/renderer/tests/basic.rs @@ -1017,3 +1017,10 @@ fn test_zero_div_zero() { let err = format!("{:?}", r.err().unwrap()); assert!(err.contains("division by zero")); } + +#[test] +fn delete_variables() { + let mut tera = Tera::default(); + let result = tera.render_str("{% set a = '1' %}{% delete a %}", &Context::new()).unwrap(); + assert_eq!(result, "".to_owned()); +}