diff --git a/tera-contrib/src/rand.rs b/tera-contrib/src/rand.rs index 2ba29ed6..be7d9952 100644 --- a/tera-contrib/src/rand.rs +++ b/tera-contrib/src/rand.rs @@ -13,6 +13,12 @@ pub fn get_random(kwargs: Kwargs, _: &State) -> TeraResult { let start = kwargs.must_get::("start")?; let end = kwargs.must_get::("end")?; + if start >= end { + return Err(tera::Error::message(format!( + "get_random: `start` ({start}) must be less than `end` ({end})." + ))); + } + match kwargs.get::("seed")? { Some(seed) => { let mut h = DefaultHasher::new(); diff --git a/tera/src/args.rs b/tera/src/args.rs index dbff5e70..3b0dd488 100644 --- a/tera/src/args.rs +++ b/tera/src/args.rs @@ -183,7 +183,7 @@ impl Kwargs { T::deserialize(&Value { inner: ValueInner::Map(self.values.clone()), }) - .map_err(|_| Error::message("Failed to deserialize")) + .map_err(Error::message) } /// Try to get the given key value and convert it to the given type diff --git a/tera/src/errors.rs b/tera/src/errors.rs index 6c117320..75a62e87 100644 --- a/tera/src/errors.rs +++ b/tera/src/errors.rs @@ -81,6 +81,13 @@ pub enum ErrorKind { /// All the parents templates we found so far inheritance_chain: Vec, }, + /// A loop was found while following `{% include %}` calls between templates + CircularInclude { + /// Name of the template where the cycle was detected + tpl: String, + /// Ordered chain of templates from `tpl` back to `tpl` + include_chain: Vec, + }, /// A template is extending a template that wasn't found in the Tera instance MissingParent { /// The template we are currently looking at @@ -138,6 +145,10 @@ impl fmt::Display for ErrorKind { f, "Circular extend detected for template '{tpl}'. Inheritance chain: `{inheritance_chain:?}`", ), + ErrorKind::CircularInclude { tpl, include_chain } => write!( + f, + "Circular include detected for template '{tpl}'. Include chain: `{include_chain:?}`", + ), ErrorKind::MissingParent { current, parent } => write!( f, "Template '{current}' is inheriting from '{parent}', which doesn't exist or isn't loaded.", @@ -230,6 +241,16 @@ impl Error { } } + pub(crate) fn circular_include(tpl: impl ToString, include_chain: Vec) -> Self { + Self { + kind: ErrorKind::CircularInclude { + tpl: tpl.to_string(), + include_chain, + }, + source: None, + } + } + pub(crate) fn missing_parent(current: impl ToString, parent: impl ToString) -> Self { Self { kind: ErrorKind::MissingParent { diff --git a/tera/src/filters.rs b/tera/src/filters.rs index 97a0c2d0..0dfd5074 100644 --- a/tera/src/filters.rs +++ b/tera/src/filters.rs @@ -220,6 +220,7 @@ pub(crate) fn title(val: &str, _: Kwargs, _: &State) -> String { res } +/// Works on char/graphemes, not bytes. pub(crate) fn truncate(val: &str, kwargs: Kwargs, _: &State) -> TeraResult { let length = kwargs.must_get::("length")?; let end = kwargs.get::<&str>("end")?.unwrap_or("…"); @@ -236,17 +237,18 @@ pub(crate) fn truncate(val: &str, kwargs: Kwargs, _: &State) -> TeraResult= val.len() { - return Ok(val.to_string()); + match val.char_indices().nth(length) { + Some((byte_idx, _)) => Ok(val[..byte_idx].to_string() + end), + None => Ok(val.to_string()), } - Ok(val[..length].to_string() + end) } } /// Return a copy of the string with each line indented by 4 spaces. /// The first line and blank lines are not indented by default. +/// Max width of 1000 to avoid DOS pub(crate) fn indent(val: &str, kwargs: Kwargs, _: &State) -> TeraResult { - let width = kwargs.get::("width")?.unwrap_or(4); + let width = kwargs.get::("width")?.unwrap_or(4).min(1000); let indent_first_line = kwargs.get::("first")?.unwrap_or(false); let indent_blank_line = kwargs.get::("blank")?.unwrap_or(false); @@ -595,7 +597,7 @@ pub(crate) fn get(val: Map, kwargs: Kwargs, _: &State) -> TeraResult { Ok(d) } else { Err(Error::message(format!( - "Map does not a key {key} and no default values were defined" + "Map does not have a key {key} and no default values were defined" ))) } } @@ -612,7 +614,7 @@ pub(crate) fn filter(val: Vec, kwargs: Kwargs, _: &State) -> TeraResult { return Err(Error::message(format!( - "Value {v} does not an attribute after following path: {attribute}" + "Value {v} does not have an attribute after following path: {attribute}" ))); } x => { @@ -637,7 +639,7 @@ pub(crate) fn group_by(val: Vec, kwargs: Kwargs, _: &State) -> TeraResult match v.get_from_path(attribute) { x if x.is_undefined() => { return Err(Error::message(format!( - "Value {v} does not an attribute after following path; {attribute}" + "Value {v} does not have an attribute after following path; {attribute}" ))); } x if x.is_none() => (), @@ -659,8 +661,6 @@ pub(crate) fn group_by(val: Vec, kwargs: Kwargs, _: &State) -> TeraResult mod tests { use super::*; use crate::Context; - #[cfg(feature = "unicode")] - use crate::Tera; use crate::value::Map; #[test] @@ -823,13 +823,25 @@ mod tests { #[cfg(feature = "unicode")] #[test] fn can_truncate_graphemes() { + let ctx = Context::new(); + let state = State::new(&ctx); let inputs = vec![("日本語", 2, "日本…"), ("👨‍👩‍👧‍👦 family", 5, "👨‍👩‍👧‍👦 fam…")]; - for (s, len, expected) in inputs { - let tpl = format!("{{{{ '{}' | truncate(length={}) }}}}", s, len); - let mut tera = Tera::default(); - tera.add_raw_template("tpl", &tpl).unwrap(); - let out = tera.render("tpl", &Context::default()).unwrap(); + for (input, length, expected) in inputs { + let out = truncate(input, Kwargs::from([("length", length.into())]), &state).unwrap(); + assert_eq!(out, expected); + } + } + + #[cfg(not(feature = "unicode"))] + #[test] + fn truncate_splits_on_char_boundary() { + let ctx = Context::new(); + let state = State::new(&ctx); + let inputs = [("😀test", 1, "😀…"), ("日本語hello", 3, "日本語…")]; + + for (input, length, expected) in inputs { + let out = truncate(input, Kwargs::from([("length", length.into())]), &state).unwrap(); assert_eq!(out, expected); } } diff --git a/tera/src/functions.rs b/tera/src/functions.rs index 617a4b3f..b604d4cd 100644 --- a/tera/src/functions.rs +++ b/tera/src/functions.rs @@ -61,6 +61,9 @@ impl StoredFunction { } } +/// Upper bound on the number of elements `range()` will produce to avoid OOM. +const MAX_RANGE_LEN: usize = 100_000; + pub(crate) fn range(kwargs: Kwargs, _: &State) -> TeraResult> { let start = kwargs.get::("start")?.unwrap_or_default(); let end = kwargs.must_get::("end")?; @@ -70,6 +73,20 @@ pub(crate) fn range(kwargs: Kwargs, _: &State) -> TeraResult> { "Function `range` was called with a `start` argument greater than the `end` one", )); } + if step_by == 0 { + return Err(Error::message( + "Function `range` was called with a `step_by` argument of 0", + )); + } + + let span = (end as i128) - (start as i128); + let step = step_by as i128; + let len = (span + step - 1) / step; + if len > MAX_RANGE_LEN as i128 { + return Err(Error::message(format!( + "Function `range` would produce {len} elements, which exceeds the limit of {MAX_RANGE_LEN}" + ))); + } Ok((start..end).step_by(step_by).collect()) } diff --git a/tera/src/parsing/compiler.rs b/tera/src/parsing/compiler.rs index d901e8c0..0969e66f 100644 --- a/tera/src/parsing/compiler.rs +++ b/tera/src/parsing/compiler.rs @@ -200,7 +200,7 @@ impl Compiler { if let Some(start) = slice.start { self.compile_expr(start); } else { - self.chunk.add(Instruction::LoadConst(0.into()), None); + self.chunk.add(Instruction::LoadConst(Value::none()), None); } if let Some(end) = slice.end { diff --git a/tera/src/parsing/lexer.rs b/tera/src/parsing/lexer.rs index f366ca2f..64221676 100644 --- a/tera/src/parsing/lexer.rs +++ b/tera/src/parsing/lexer.rs @@ -265,7 +265,7 @@ fn basic_tokenize( ($num_bytes:expr) => {{ let (skipped, new_rest) = rest.split_at($num_bytes); for c in skipped.chars() { - current_byte += 1; + current_byte += c.len_utf8(); match c { '\n' => { current_line += 1; diff --git a/tera/src/snapshot_tests/build_errors/circular_include.txt b/tera/src/snapshot_tests/build_errors/circular_include.txt new file mode 100644 index 00000000..c27a1e37 --- /dev/null +++ b/tera/src/snapshot_tests/build_errors/circular_include.txt @@ -0,0 +1,4 @@ +$$ a +{% include "b" %} +$$ b +{% include "a" %} diff --git a/tera/src/snapshot_tests/build_errors/circular_include_intermediate.txt b/tera/src/snapshot_tests/build_errors/circular_include_intermediate.txt new file mode 100644 index 00000000..4a6faf31 --- /dev/null +++ b/tera/src/snapshot_tests/build_errors/circular_include_intermediate.txt @@ -0,0 +1,6 @@ +$$ a +{% include "b" %} +$$ b +{% include "c" %} +$$ c +{% include "b" %} diff --git a/tera/src/snapshot_tests/build_errors/including_itself.txt b/tera/src/snapshot_tests/build_errors/including_itself.txt new file mode 100644 index 00000000..16decb7f --- /dev/null +++ b/tera/src/snapshot_tests/build_errors/including_itself.txt @@ -0,0 +1,2 @@ +$$ a +{% include "a" %} diff --git a/tera/src/snapshot_tests/rendering_inputs/errors/slice_step_zero.txt b/tera/src/snapshot_tests/rendering_inputs/errors/slice_step_zero.txt new file mode 100644 index 00000000..6cf46fa1 --- /dev/null +++ b/tera/src/snapshot_tests/rendering_inputs/errors/slice_step_zero.txt @@ -0,0 +1 @@ +{{ numbers[0:2:0] }} diff --git a/tera/src/snapshot_tests/rendering_inputs/success/indexing.txt b/tera/src/snapshot_tests/rendering_inputs/success/indexing.txt index b56f3932..6559e1bb 100644 --- a/tera/src/snapshot_tests/rendering_inputs/success/indexing.txt +++ b/tera/src/snapshot_tests/rendering_inputs/success/indexing.txt @@ -5,11 +5,20 @@ {{ numbers[:2] }} {{ numbers[1:2] }} {{ numbers[0:2:2] }} +{{ numbers[::2] }} +{{ numbers[::-2] }} +{{ numbers[10:20] }} {{ numbers[::-1] }} +{{ product.name[::2] }} {{ product.name[-1] }} {{ product.name[::-1] }} {{ product.name[1:] }} {{ product.name[:-1] }} {{ (numbers | reverse)[0] }} {{ range(end=5)[2] }} -{{ (numbers | reverse)[:-1] | first }} \ No newline at end of file +{{ (numbers | reverse)[:-1] | first }} +{{ [1,2,3][10::-1] }} +{{ [1,2,3][1000:] }} +{{ [1,2,3][:1000] }} +{{ [1,2,3][-100:] }} +{{ [1,2,3][:-100:-1] }} \ No newline at end of file diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@circular_include.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@circular_include.txt.snap new file mode 100644 index 00000000..ba1e96b0 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@circular_include.txt.snap @@ -0,0 +1,6 @@ +--- +source: tera/src/snapshot_tests/build_errors.rs +expression: "&err" +input_file: tera/src/snapshot_tests/build_errors/circular_include.txt +--- +Circular include detected for template 'a'. Include chain: `["a", "b", "a"]` diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@circular_include_intermediate.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@circular_include_intermediate.txt.snap new file mode 100644 index 00000000..b8bc04b2 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@circular_include_intermediate.txt.snap @@ -0,0 +1,6 @@ +--- +source: tera/src/snapshot_tests/build_errors.rs +expression: "&err" +input_file: tera/src/snapshot_tests/build_errors/circular_include_intermediate.txt +--- +Circular include detected for template 'b'. Include chain: `["a", "b", "c", "b"]` diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@including_itself.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@including_itself.txt.snap new file mode 100644 index 00000000..8f0496b9 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__build_errors__build_errors@including_itself.txt.snap @@ -0,0 +1,6 @@ +--- +source: tera/src/snapshot_tests/build_errors.rs +expression: "&err" +input_file: tera/src/snapshot_tests/build_errors/including_itself.txt +--- +Circular include detected for template 'a'. Include chain: `["a", "a"]` diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_templates_success@tpl_simple.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_templates_success@tpl_simple.txt.snap index 4e01e9d3..553e52fe 100644 --- a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_templates_success@tpl_simple.txt.snap +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__parser__parser_templates_success@tpl_simple.txt.snap @@ -47,15 +47,15 @@ input_file: tera/src/snapshot_tests/parser_inputs/success/tpl/tpl_simple.txt left: GetAttr { expr: Var { name: "product", - } @ 9:11-9:18 (207..214), + } @ 9:11-9:18 (208..215), name: "price", optional: false, - } @ 9:19-9:24 (215..220), - right: 1.2 @ 9:27-9:31 (223..227), - } @ 9:11-9:31 (207..227), + } @ 9:19-9:24 (216..221), + right: 1.2 @ 9:27-9:31 (224..228), + } @ 9:11-9:31 (208..228), " (VAT inc.)

\n

Look at reviews from your friends ", Var { name: "username", - } @ 10:44-10:52 (290..298), + } @ 10:44-10:52 (291..299), "

\n \n \n", ] diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slice_step_zero.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slice_step_zero.txt.snap new file mode 100644 index 00000000..1027feb2 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slice_step_zero.txt.snap @@ -0,0 +1,10 @@ +--- +source: tera/src/snapshot_tests/rendering.rs +expression: "&err" +input_file: tera/src/snapshot_tests/rendering_inputs/errors/slice_step_zero.txt +--- +error: Slicing step cannot be 0 + --> slice_step_zero.txt:1:3 + | +1 | {{ numbers[0:2:0] }} + | ^^^^^^^ diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap index c05ed9f5..9be6e119 100644 --- a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap @@ -9,8 +9,12 @@ input_file: tera/src/snapshot_tests/rendering_inputs/success/indexing.txt [1, 2] [1, 2] [2] -[1, 2] +[1] +[1, 3] +[3, 1] +[] [3, 2, 1] +Mt G G otoM oto G @@ -18,3 +22,8 @@ Moto 3 2 3 +[3, 2, 1] +[] +[1, 2, 3] +[1, 2, 3] +[3, 2, 1] diff --git a/tera/src/template.rs b/tera/src/template.rs index 86699308..e40c9e4e 100644 --- a/tera/src/template.rs +++ b/tera/src/template.rs @@ -141,6 +141,30 @@ impl Template { } } +/// Recursive fn that finds all the includes to detect if there are some cycles +pub(crate) fn check_include_cycles(tera: &Tera, start: &Template) -> Result<(), Error> { + let mut stack: Vec = vec![start.name.clone()]; + fn walk(tera: &Tera, current: &Template, stack: &mut Vec) -> Result<(), Error> { + let mut names: Vec<&String> = current.include_calls.keys().collect(); + names.sort(); + for include_name in names { + let Some(resolved) = tera.resolve_template_name(include_name) else { + continue; + }; + if stack.iter().any(|s| s == resolved) { + let mut chain = stack.clone(); + chain.push(resolved.to_string()); + return Err(Error::circular_include(resolved, chain)); + } + stack.push(resolved.to_string()); + walk(tera, &tera.templates[resolved], stack)?; + stack.pop(); + } + Ok(()) + } + walk(tera, start, &mut stack) +} + /// Recursive fn that finds all the parents and put them in an ordered Vec from closest to first parent /// parent template pub(crate) fn find_parents( diff --git a/tera/src/tera.rs b/tera/src/tera.rs index 9260a083..1ea5573a 100644 --- a/tera/src/tera.rs +++ b/tera/src/tera.rs @@ -8,7 +8,7 @@ use crate::args::ArgFromValue; use crate::errors::{Error, ReportError, TeraResult}; use crate::filters::{Filter, StoredFilter}; use crate::functions::{Function, StoredFunction}; -use crate::template::{Template, find_parents}; +use crate::template::{Template, check_include_cycles, find_parents}; use crate::tests::{StoredTest, Test, TestResult}; use crate::value::FunctionResult; use crate::value::Value; @@ -505,8 +505,13 @@ impl Tera { let mut component_sources: HashMap<&str, (&str, usize)> = HashMap::new(); // 1st loop: find parents of each template and check for duplicate components - for (name, tpl) in &self.templates { + // Sort so error messages (circular include chains, etc.) are deterministic + let mut ordered_names: Vec<&String> = self.templates.keys().collect(); + ordered_names.sort(); + for name in ordered_names { + let tpl = &self.templates[name]; let parents = find_parents(self, tpl, tpl, vec![])?; + check_include_cycles(self, tpl)?; for component_name in tpl.components.keys() { let current_priority = self.get_template_priority(&tpl.name); @@ -626,7 +631,7 @@ impl Tera { // 3rd loop: we actually set everything we've done on the templates objects for (name, tpl) in self.templates.iter_mut() { - tpl.raw_content_num_bytes += tpl_size_hint.remove(name.as_str()).unwrap(); + tpl.raw_content_num_bytes = tpl_size_hint.remove(name.as_str()).unwrap(); tpl.parents = tpl_parents.remove(name.as_str()).unwrap(); tpl.block_lineage = tpl_blocks.remove(name.as_str()).unwrap(); } @@ -656,10 +661,7 @@ impl Tera { /// tera.add_raw_template("new.html", "Blabla").unwrap(); /// ``` pub fn add_raw_template(&mut self, name: &str, content: &str) -> TeraResult<()> { - let template = Template::new(name, content, None, self.delimiters)?; - self.templates.insert(name.to_string(), template); - self.finalize_templates()?; - Ok(()) + self.add_raw_templates(std::iter::once((name, content))) } /// Add all the templates given to the Tera instance @@ -681,22 +683,47 @@ impl Tera { N: AsRef, C: AsRef, { - for (name, content) in templates { - let template = Template::new(name.as_ref(), content.as_ref(), None, self.delimiters)?; - self.templates.insert(name.as_ref().to_string(), template); + let mut inserted: Vec<(String, Option