Skip to content
Merged

Fixes #110

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tera-contrib/src/rand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ pub fn get_random(kwargs: Kwargs, _: &State) -> TeraResult<i64> {
let start = kwargs.must_get::<i64>("start")?;
let end = kwargs.must_get::<i64>("end")?;

if start >= end {
return Err(tera::Error::message(format!(
"get_random: `start` ({start}) must be less than `end` ({end})."
)));
}

match kwargs.get::<String>("seed")? {
Some(seed) => {
let mut h = DefaultHasher::new();
Expand Down
2 changes: 1 addition & 1 deletion tera/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions tera/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ pub enum ErrorKind {
/// All the parents templates we found so far
inheritance_chain: Vec<String>,
},
/// 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<String>,
},
/// A template is extending a template that wasn't found in the Tera instance
MissingParent {
/// The template we are currently looking at
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -230,6 +241,16 @@ impl Error {
}
}

pub(crate) fn circular_include(tpl: impl ToString, include_chain: Vec<String>) -> 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 {
Expand Down
40 changes: 26 additions & 14 deletions tera/src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
let length = kwargs.must_get::<usize>("length")?;
let end = kwargs.get::<&str>("end")?.unwrap_or("…");
Expand All @@ -236,17 +237,18 @@ pub(crate) fn truncate(val: &str, kwargs: Kwargs, _: &State) -> TeraResult<Strin

#[cfg(not(feature = "unicode"))]
{
if length >= 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<String> {
let width = kwargs.get::<usize>("width")?.unwrap_or(4);
let width = kwargs.get::<usize>("width")?.unwrap_or(4).min(1000);
let indent_first_line = kwargs.get::<bool>("first")?.unwrap_or(false);
let indent_blank_line = kwargs.get::<bool>("blank")?.unwrap_or(false);

Expand Down Expand Up @@ -595,7 +597,7 @@ pub(crate) fn get(val: Map, kwargs: Kwargs, _: &State) -> TeraResult<Value> {
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"
)))
}
}
Expand All @@ -612,7 +614,7 @@ pub(crate) fn filter(val: Vec<Value>, kwargs: Kwargs, _: &State) -> TeraResult<V
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 => {
Expand All @@ -637,7 +639,7 @@ pub(crate) fn group_by(val: Vec<Value>, 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() => (),
Expand All @@ -659,8 +661,6 @@ pub(crate) fn group_by(val: Vec<Value>, kwargs: Kwargs, _: &State) -> TeraResult
mod tests {
use super::*;
use crate::Context;
#[cfg(feature = "unicode")]
use crate::Tera;
use crate::value::Map;

#[test]
Expand Down Expand Up @@ -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);
}
}
Expand Down
17 changes: 17 additions & 0 deletions tera/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<isize>> {
let start = kwargs.get::<isize>("start")?.unwrap_or_default();
let end = kwargs.must_get::<isize>("end")?;
Expand All @@ -70,6 +73,20 @@ pub(crate) fn range(kwargs: Kwargs, _: &State) -> TeraResult<Vec<isize>> {
"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())
}
Expand Down
2 changes: 1 addition & 1 deletion tera/src/parsing/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tera/src/parsing/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions tera/src/snapshot_tests/build_errors/circular_include.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$$ a
{% include "b" %}
$$ b
{% include "a" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$$ a
{% include "b" %}
$$ b
{% include "c" %}
$$ c
{% include "b" %}
2 changes: 2 additions & 0 deletions tera/src/snapshot_tests/build_errors/including_itself.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$$ a
{% include "a" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ numbers[0:2:0] }}
11 changes: 10 additions & 1 deletion tera/src/snapshot_tests/rendering_inputs/success/indexing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
{{ (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] }}
Original file line number Diff line number Diff line change
@@ -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"]`
Original file line number Diff line number Diff line change
@@ -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"]`
Original file line number Diff line number Diff line change
@@ -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"]`
Original file line number Diff line number Diff line change
Expand Up @@ -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.)</p>\n <p>Look at reviews from your friends ",
Var {
name: "username",
} @ 10:44-10:52 (290..298),
} @ 10:44-10:52 (291..299),
"</p>\n <button>Buy!</button>\n </body>\n</html>",
]
Original file line number Diff line number Diff line change
@@ -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] }}
| ^^^^^^^
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ 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
Moto
3
2
3
[3, 2, 1]
[]
[1, 2, 3]
[1, 2, 3]
[3, 2, 1]
24 changes: 24 additions & 0 deletions tera/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = vec![start.name.clone()];
fn walk(tera: &Tera, current: &Template, stack: &mut Vec<String>) -> 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(
Expand Down
Loading