Repository: Keats/tera (rust)

Issue:
The template's `in` doesn't work when a slash (/) exists due to escaping
Given the Rust + template

```
context.insert("friends", &vec!["https://Bob", "Kate"]);
...
Output: {% if "https://Bob" in friends %}TRUE{% endif %}
```

the output will be blank.

If we instead run:

- `{% if "https://Bob" in ["https://Bob", "Kate"] %}`
- `{% if "https://Bob"|safe in friends %}TRUE{% endif %}`

then the statement works as expected.

---

After [adding a test to ensure I wasn't mad](https://github.com/Smerity/tera/commit/2ccb8785f18cb55d82f0fdc7bf5ab66ec00583b0) I traced the code for running an `in` to `eval_in_condition` which calls `eval_expression` on the `lhs` and `rhs` inputs. As our example has an `ExprVal::String` on the left we end up escaping `https://Bob`. Thus, to run the above correctly we need to instead run:

```
context.insert("friends", &vec!["https://Bob", "Kate"]);
...
Output: {% if "https://Bob"|safe in friends %}TRUE{% endif %}
```

This works as we prevent the `ExprVal::String` from being escaped.

**If this is expected behaviour:** I'll add a section to the [containing docs](https://tera.netlify.app/docs/#containing)

**If it isn't expected behaviour:** I can investigate this issue further (though would appreciate someone with more Tera knowledge as I'm uncertain how problematic making the `lhs` / `rhs` "unsafe" (i.e. skipping the safety) might be)

Pull request description:
eval_in_condition: Use safe_eval rather than eval (#738)
Fixes #738

- Added two simple tests to ensure there isn't a regression
- The fix is simple though would appreciate a sanity check, specifically that calling `safe_eval_expression` in `eval_in_condition` can't result in safety issues
  - I don't believe it does but a double check from someone more familiarly with the codebase would be appreciated =]

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e6e6d16..a1384fc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,7 +15,7 @@ jobs:
         include:
         - build: pinned
           os: ubuntu-18.04
-          rust: 1.46.0
+          rust: 1.56.0
         - build: stable
           os: ubuntu-18.04
           rust: stable
diff --git a/Cargo.toml b/Cargo.toml
index 60c3baa..d63375b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,7 +31,7 @@ percent-encoding = {version = "2.1", optional = true}
 # used in filesizeformat filter
 humansize = {version = "1", optional = true}
 # used in date format filter
-chrono = {version = "0.4.1", optional = true, default-features = false, features = ["std", "clock"]}
+chrono = {version = "0.4.20", optional = true, default-features = false, features = ["std", "clock"]}
 chrono-tz = {version = "0.6", optional = true}
 # used in get_random function
 rand = {version = "0.8", optional = true}
diff --git a/src/parser/tera.pest b/src/parser/tera.pest
index e723928..7c6da9f 100644
--- a/src/parser/tera.pest
+++ b/src/parser/tera.pest
@@ -86,7 +86,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_expr = { ("(" ~ basic_expr ~ ")" | basic_val) ~ (basic_op ~ basic_val)* }
+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/renderer/processor.rs b/src/renderer/processor.rs
index 2c1f2db..27a7982 100644
--- a/src/renderer/processor.rs
+++ b/src/renderer/processor.rs
@@ -303,8 +303,8 @@ impl<'a> Processor<'a> {
     }
 
     fn eval_in_condition(&mut self, in_cond: &'a In) -> Result<bool> {
-        let lhs = self.eval_expression(&in_cond.lhs)?;
-        let rhs = self.eval_expression(&in_cond.rhs)?;
+        let lhs = self.safe_eval_expression(&in_cond.lhs)?;
+        let rhs = self.safe_eval_expression(&in_cond.rhs)?;
 
         let present = match *rhs {
             Value::Array(ref v) => v.contains(&lhs),


Test patch:
diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs
index d9e1b34..0091f12 100644
--- a/src/renderer/tests/basic.rs
+++ b/src/renderer/tests/basic.rs
@@ -124,6 +124,10 @@ fn render_variable_block_ident() {
         ("{{ 1.9 + a | round - 1 }}", "3"),
         ("{{ 1.9 + a | round - 1.8 + a | round }}", "0"),
         ("{{ 1.9 + a | round - 1.8 + a | round - 1 }}", "-1"),
+        ("{{ 4 + 40 / (2 + 8) / 4 }}", "5"),
+        ("{{ ( ( 2 ) + ( 2 ) ) }}", "4"),
+        ("{{ ( ( 4 / 1 ) + ( 2 / 1 ) ) }}", "6"),
+        ("{{ ( ( 4 + 2 ) / ( 2 + 1 ) ) }}", "2"),
         // https://github.com/Keats/tera/issues/435
         (
             "{{ with_newline | replace(from='\n', to='<br>') | safe }}",
@@ -151,6 +155,7 @@ fn render_variable_block_logic_expr() {
     hashmap.insert("b", 10);
     hashmap.insert("john", 100);
     context.insert("object", &hashmap);
+    context.insert("urls", &vec!["https://test"]);
 
     let inputs = vec![
         ("{{ (1.9 + a) | round > 10 }}", "false"),
@@ -177,6 +182,8 @@ fn render_variable_block_logic_expr() {
         ("{{ name not in 'hello' }}", "true"),
         ("{{ name in ['bob', 2, 'john'] }}", "true"),
         ("{{ a in ['bob', 2, 'john'] }}", "true"),
+        ("{{ \"https://test\" in [\"https://test\"] }}", "true"),
+        ("{{ \"https://test\" in urls }}", "true"),
         ("{{ 'n' in name }}", "true"),
         ("{{ '<' in malicious }}", "true"),
         ("{{ 'a' in object }}", "true"),