Merge pull request #304 from Kijewski/pr-excessive-filter-block

parser: fix excessive filter blocks
This commit is contained in:
René Kijewski 2025-01-04 11:53:26 +01:00 committed by GitHub
commit fff55f2a1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 267 additions and 20 deletions

View File

@ -29,6 +29,14 @@ macro_rules! this_file {
}
}
impl fmt::Display for Scenario<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
$(Self::$ty(scenario) => fmt::Display::fmt(scenario, f),)*
}
}
}
#[derive(Arbitrary)]
enum Target {
$($ty,)*

View File

@ -1,3 +1,5 @@
use std::fmt;
use arbitrary::{Arbitrary, Unstructured};
use rinja::filters;
@ -44,6 +46,42 @@ fn run_text(filter: Text<'_>) -> Result<(), rinja::Error> {
Ok(())
}
impl fmt::Display for Scenario<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Scenario::Text(Text { input, filter }) = self;
let text = match filter {
TextFilter::Capitalize => format!("capitalize({input:?})"),
TextFilter::Center(a) => format!("center({input:?}, {a:?})"),
TextFilter::Indent(a) => format!("indent({input:?}, {a:?})"),
TextFilter::Linebreaks => format!("linebreaks({input:?})"),
TextFilter::LinebreaksBr => format!("linebreaksbr({input:?})"),
TextFilter::Lowercase => format!("lowercase({input:?})"),
TextFilter::ParagraphBreaks => format!("paragraphbreaks({input:?})"),
TextFilter::Safe(e) => match e {
Escaper::Html => format!("safe({input:?}, filters::Html)"),
Escaper::Text => format!("safe({input:?}, filters::Text)"),
},
TextFilter::Title => format!("title({input:?})"),
TextFilter::Trim => format!("trim({input:?})"),
TextFilter::Truncate(a) => format!("truncate({input:?}, {a:?})"),
TextFilter::Uppercase => format!("uppercase({input:?})"),
TextFilter::Urlencode => format!("urlencode({input:?})"),
TextFilter::UrlencodeStrict => format!("urlencode_strict({input:?})"),
};
write!(
f,
"\
use rinja::filters;
#[test]
fn test() {{
let _: String = filters::{text}?.to_string();
}}\
",
)
}
}
#[derive(Arbitrary, Debug, Clone, Copy)]
pub struct Text<'a> {
input: &'a str,

View File

@ -1,6 +1,8 @@
#[allow(clippy::module_inception)]
mod html;
use std::fmt;
use arbitrary::{Arbitrary, Unstructured};
use html_escape::decode_html_entities_to_string;
@ -41,3 +43,45 @@ impl<'a> super::Scenario<'a> for Scenario<'a> {
Ok(())
}
}
impl fmt::Display for Scenario<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Scenario::String(src) => {
write!(
f,
"\
#[test]
fn test() {{
let mut dest = String::with_capacity({len});
html::write_escaped_str(&mut dest, {src:?}).unwrap();
let mut unescaped = String::with_capacity(src.len());
let unescaped = html_escape::decode_html_entities_to_string(dest, &mut unescaped);
assert_eq!(src, unescaped);
}}\
",
len = src.len(),
)
}
Scenario::Char(c) => {
write!(
f,
"\
#[test]
fn test() {{
let mut dest = String::with_capacity(6);
html::write_escaped_char(&mut dest, {c:?}).unwrap();
let mut src = [0; 4];
let src = c.encode_utf8(&mut src);
let mut unescaped = String::with_capacity(4);
let unescaped = decode_html_entities_to_string(dest, &mut unescaped);
assert_eq!(src, unescaped);
}}\
",
)
}
}
}
}

View File

@ -23,7 +23,7 @@ pub const TARGETS: &[(&str, TargetBuilder)] = &[
pub type TargetBuilder = for<'a> fn(&'a [u8]) -> Result<NamedTarget<'a>, arbitrary::Error>;
pub trait Scenario<'a>: fmt::Debug + Sized {
pub trait Scenario<'a>: fmt::Debug + fmt::Display + Sized {
type RunError: Error + Send + 'static;
fn fuzz(data: &'a [u8]) -> Result<(), FuzzError<Self::RunError>> {
@ -76,6 +76,13 @@ impl fmt::Debug for NamedTarget<'_> {
}
}
impl fmt::Display for NamedTarget<'_> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.as_test(f)
}
}
impl<'a> NamedTarget<'a> {
#[inline]
fn new<S: Scenario<'a> + 'a>(data: &'a [u8]) -> Result<Self, arbitrary::Error> {
@ -86,13 +93,20 @@ impl<'a> NamedTarget<'a> {
trait RunScenario<'a> {
fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
fn as_test(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
fn run(&self) -> Result<(), Box<dyn Error + Send + 'static>>;
}
impl<'a, T: Scenario<'a>> RunScenario<'a> for T {
#[inline]
fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt(f)
fmt::Debug::fmt(self, f)
}
#[inline]
fn as_test(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
#[inline]

View File

@ -1,5 +1,8 @@
use std::borrow::Cow;
use std::fmt;
use arbitrary::{Arbitrary, Unstructured};
use rinja_parser::{Ast, Syntax, SyntaxBuilder};
use rinja_parser::{Ast, InnerSyntax, Syntax, SyntaxBuilder};
#[derive(Debug, Default)]
pub struct Scenario<'a> {
@ -36,7 +39,50 @@ impl<'a> super::Scenario<'a> for Scenario<'a> {
fn run(&self) -> Result<(), Self::RunError> {
let Scenario { syntax, src } = self;
Ast::from_str(src, None, syntax)?;
let _: Ast<'_> = Ast::from_str(src, None, syntax)?;
Ok(())
}
}
impl fmt::Display for Scenario<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Scenario { syntax, src } = self;
let syntax = if *syntax == Syntax::default() {
Cow::Borrowed("Syntax::default()")
} else {
let InnerSyntax {
block_start,
block_end,
expr_start,
expr_end,
comment_start,
comment_end,
} = **syntax;
Cow::Owned(format!(
"\
SyntaxBuilder {{
name: \"test\",
block_start: {block_start:?},
block_end: {block_end:?},
expr_start: {expr_start:?},
expr_end: {expr_end:?},
comment_start: {comment_start:?},
comment_end: {comment_end:?},
}}.to_syntax().unwrap()",
))
};
write!(
f,
"\
use rinja_parser::{{Ast, ParseError}};
#[test]
fn test() -> Result<(), ParseError> {{
let src = {src:?};
let syntax = {syntax};
Ast::from_str(src, None, &syntax).map(|_| ())
}}\
",
)
}
}

View File

@ -1,6 +1,6 @@
use std::env::args_os;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::{Read, Write, stdin};
use std::path::{Path, PathBuf};
use fuzz::{DisplayTargets, TARGETS};
@ -9,10 +9,11 @@ fn main() -> Result<(), Error> {
let mut args = args_os().fuse();
let exe = args.next().map(PathBuf::from);
let name = args.next().and_then(|s| s.into_string().ok());
let path = args.next().map(PathBuf::from);
let src = args.next().map(PathBuf::from);
let dest = args.next().map(PathBuf::from);
let empty = args.next().map(|_| ());
let (Some(name), Some(path), None) = (name, path, empty) else {
let (Some(name), Some(src), None) = (name, src, empty) else {
return Err(Error::Usage(exe));
};
@ -22,37 +23,62 @@ fn main() -> Result<(), Error> {
.ok_or(Error::Target(name))?;
let mut data = Vec::new();
match OpenOptions::new().read(true).open(Path::new(&path)) {
Ok(mut f) => {
f.read_to_end(&mut data)
.map_err(|err| Error::Read(err, path))?;
if src == Path::new("-") {
stdin().read_to_end(&mut data).map_err(Error::Stdin)?;
} else {
match OpenOptions::new().read(true).open(Path::new(&src)) {
Ok(mut f) => {
f.read_to_end(&mut data)
.map_err(|err| Error::Read(err, src))?;
}
Err(err) => return Err(Error::Open(err, src)),
}
Err(err) => return Err(Error::Open(err, path)),
};
}
let scenario = scenario_builder(&data).map_err(Error::Build)?;
eprintln!("{scenario:#?}");
scenario.run().map_err(Error::Run)?;
println!("Success.");
if let Some(dest) = dest {
if dest == Path::new("-") {
println!("{scenario}");
} else {
let mut f = match OpenOptions::new().write(true).create_new(true).open(&dest) {
Ok(f) => f,
Err(err) => return Err(Error::DestOpen(err, dest)),
};
writeln!(f, "{scenario}").map_err(|err| Error::DestWrite(err, dest))?;
}
} else {
eprintln!("{scenario:#?}");
scenario.run().map_err(Error::Run)?;
println!("Success.");
}
Ok(())
}
#[derive(thiserror::Error, pretty_error_debug::Debug)]
enum Error {
#[error(
"wrong arguments supplied\nUsage: {} <{DisplayTargets}> <path>",
"wrong arguments supplied\n\
Usage: {} <target> <src> [<dest>]\n\
* <target> {DisplayTargets}\n\
* <src> failed scenario (supply '-' to from from STDIN)\n\
* <dest> write a #[test] to this file (optional; supply '-' to write to STDOUT)",
.0.as_deref().unwrap_or(Path::new("rinja_fuzzing")).display(),
)]
Usage(Option<PathBuf>),
#[error("unknown fuzzing target {:?}\nImplemented targets: {DisplayTargets}", .0)]
Target(String),
#[error("could not open input file {}", .1.display())]
#[error("could not read standard input")]
Stdin(#[source] std::io::Error),
#[error("could not open input file {:?}", .1.display())]
Open(#[source] std::io::Error, PathBuf),
#[error("could not read opened input file {}", .1.display())]
#[error("could not read opened input file {:?}", .1.display())]
Read(#[source] std::io::Error, PathBuf),
#[error("could not build scenario")]
Build(#[source] arbitrary::Error),
#[error("could not run scenario")]
Run(#[source] Box<dyn std::error::Error + Send + 'static>),
#[error("could could not create destination file {:?} for writing", .1.display())]
DestOpen(#[source] std::io::Error, PathBuf),
#[error("could could not write to opened destination file {:?}", .1.display())]
DestWrite(#[source] std::io::Error, PathBuf),
}

View File

@ -765,6 +765,7 @@ pub struct FilterBlock<'a> {
impl<'a> FilterBlock<'a> {
fn parse(i: &mut &'a str, s: &State<'_, '_>) -> ParseResult<'a, WithSpan<'a, Self>> {
let start_s = *i;
let mut level_guard = s.level.guard();
let mut start = (
opt(Whitespace::parse),
ws(keyword("filter")),
@ -774,6 +775,8 @@ impl<'a> FilterBlock<'a> {
ws(identifier),
opt(|i: &mut _| Expr::arguments(i, s.level, false)),
repeat(0.., |i: &mut _| {
#[allow(clippy::explicit_auto_deref)] // false positive
level_guard.nest(*i)?;
let start = *i;
filter(i, s.level).map(|(name, params)| (name, params, start))
})

View File

@ -1136,3 +1136,20 @@ fn fuzzed_span_is_not_substring_of_source() {
&Syntax::default(),
);
}
#[test]
fn fuzzed_excessive_filter_block() {
let src = include_str!("../tests/excessive_filter_block.txt");
let err = Ast::from_str(src, None, &Syntax::default()).unwrap_err();
assert_eq!(
err.to_string().lines().next(),
Some("your template code is too deeply nested, or the last expression is too complex"),
);
let src = include!("../tests/fuzzed_excessive_filter_block.inc");
let err = Ast::from_str(src, None, &Syntax::default()).unwrap_err();
assert_eq!(
err.to_string().lines().next(),
Some("your template code is too deeply nested, or the last expression is too complex"),
);
}

View File

@ -0,0 +1,50 @@
{% filter x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|
x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x %} {% endfilter %}

File diff suppressed because one or more lines are too long