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