Compare commits

...

4 commits

Author SHA1 Message Date
3f549690c1 Fix combinatorial explosion on backtracking broken if chains
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 16:21:23 +01:00
b0620a00d5 Fix issue with repeating {{ else }} blocks
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 16:02:55 +01:00
462355b6f2 Fix invalid indices when content is not long enough
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 15:20:07 +01:00
fa1582f3ad Add fuzzing
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 15:20:02 +01:00
14 changed files with 420 additions and 34 deletions

View file

@ -95,6 +95,10 @@
pkgs.cargo-flamegraph pkgs.cargo-flamegraph
]; ];
}; };
devShells.fuzz = devShells.crate.overrideAttrs (prev: {
nativeBuildInputs = [ unstableRustTarget pkgs.cargo-fuzz ];
});
} }
); );
} }

1
fuzz/.envrc Normal file
View file

@ -0,0 +1 @@
use flake .#fuzz

6
fuzz/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.direnv/
target
corpus
artifacts
coverage

277
fuzz/Cargo.lock generated Normal file
View file

@ -0,0 +1,277 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "annotate-snippets"
version = "0.12.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fc7650eedcb2fee505aad48491529e408f0e854c2d9f63eb86c1361b9b3f93"
dependencies = [
"anstyle",
"memchr",
"unicode-width",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "cc"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libfuzzer-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "nomo"
version = "0.1.0"
dependencies = [
"annotate-snippets",
"displaydoc",
"serde",
"serde_json",
"thiserror",
"winnow",
]
[[package]]
name = "nomo-fuzz"
version = "0.0.0"
dependencies = [
"libfuzzer-sys",
"nomo",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

21
fuzz/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "nomo-fuzz"
version = "0.0.0"
publish = false
edition = "2024"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.nomo]
path = ".."
[[bin]]
name = "fuzz_target_1"
path = "fuzz_targets/fuzz_target_1.rs"
test = false
doc = false
bench = false

View file

@ -0,0 +1,18 @@
#![no_main]
use libfuzzer_sys::Corpus;
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: String| -> Corpus {
let Ok(parsed) = nomo::parser::parse(data.into()) else {
return Corpus::Reject;
};
let Ok(ast) = nomo::ast::parse(parsed.tokens()) else {
return Corpus::Keep;
};
let _instructions = nomo::emit::emit_machine(ast);
Corpus::Keep
});

View file

@ -345,39 +345,67 @@ fn parse_conditional_chain<'input>(
chain.push(if_block); chain.push(if_block);
loop { let content = resume_after_cut(
let (content, end_block): (Vec<_>, _) = repeat_till( cut_err(inner_conditional_chain),
0.., repeat_till(0.., any, parse_end).map(|((), _)| ()),
parse_ast, )
trace( .parse_next(input)?;
"conditional_chain_else/end",
alt((parse_end, parse_conditional_else)),
),
)
.parse_next(input)?;
chain.push(TemplateAstExpr::ConditionalContent { content }); chain.extend(content.into_iter().flatten());
let is_end = if let TemplateAstExpr::Block { ref expression, .. } = end_block
&& let TemplateAstExpr::EndBlock = &**expression
{
true
} else {
false
};
chain.push(end_block);
if is_end {
break;
}
}
Ok(TemplateAstExpr::ConditionalChain { chain }) Ok(TemplateAstExpr::ConditionalChain { chain })
}) })
.parse_next(input) .parse_next(input)
} }
fn inner_conditional_chain<'input>(
input: &mut Input<'input>,
) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
let mut needs_end = false;
let mut chain = vec![];
loop {
let (content, end_block): (Vec<_>, _) = repeat_till(
0..,
parse_ast,
trace(
"conditional_chain_else/end",
alt((parse_end, parse_conditional_else)),
),
)
.parse_next(input)?;
chain.push(TemplateAstExpr::ConditionalContent { content });
let is_end = if let TemplateAstExpr::Block { ref expression, .. } = end_block
&& let TemplateAstExpr::EndBlock = &**expression
{
true
} else {
false
};
if !is_end && needs_end {
return Err(AstError::from_input(input));
}
if let TemplateAstExpr::Block { expression, .. } = &end_block
&& let TemplateAstExpr::ElseConditional { expression: None } = &**expression
{
needs_end = true;
}
chain.push(end_block);
if is_end {
break;
}
}
Ok(chain)
}
fn parse_conditional_if<'input>( fn parse_conditional_if<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {

View file

@ -123,14 +123,16 @@ fn emit_ast_expr(
{ {
previous_post_whitespace_content = post_whitespace_content; previous_post_whitespace_content = post_whitespace_content;
if let Some(ws) = prev_whitespace_content { if let Some(ws) = prev_whitespace_content {
let idx = end_indices.last().copied();
eval.insert( eval.insert(
eval.len() - 2, idx.unwrap_or(eval.len()),
Instruction::AppendContent { Instruction::AppendContent {
content: ws.source().clone(), content: ws.source().clone(),
}, },
); );
let index_index = end_indices.len() - 1; if let Some(idx) = end_indices.last_mut() {
end_indices[index_index] += 1; *idx += 1;
}
} }
if let TemplateAstExpr::IfConditional { expression } = &**expression { if let TemplateAstExpr::IfConditional { expression } = &**expression {

View file

@ -383,10 +383,10 @@ fn parse_block_token<'input>(input: &mut Input<'input>) -> PResult<'input, Templ
"parse_block_token", "parse_block_token",
alt(( alt((
parse_ident, parse_ident,
parse_literal, terminated(parse_literal, ident_terminator_check),
parse_condition_if, terminated(parse_condition_if, ident_terminator_check),
parse_condition_else, terminated(parse_condition_else, ident_terminator_check),
parse_end, terminated(parse_end, ident_terminator_check),
parse_whitespace, parse_whitespace,
)), )),
) )

18
tests/checks.rs Normal file
View file

@ -0,0 +1,18 @@
#[test]
fn check_files() {
let files = std::fs::read_dir("tests/checks/").unwrap();
for file in files {
let input = std::fs::read_to_string(file.unwrap().path()).unwrap();
let Ok(parsed) = nomo::parser::parse(input.into()) else {
continue;
};
let Ok(ast) = nomo::ast::parse(parsed.tokens()) else {
continue;
};
let _emit = nomo::emit::emit_machine(ast);
}
}

BIN
tests/checks/long.nomo Normal file

Binary file not shown.

View file

@ -0,0 +1,2 @@
{{if en}}{{ end}}

View file

@ -0,0 +1,3 @@
{{ if t }}
{{else}}{{ else }}
{{end }}

View file

@ -31,7 +31,13 @@ fn check_cases() {
insta::assert_debug_snapshot!("1-parsed", parsed); insta::assert_debug_snapshot!("1-parsed", parsed);
let ast = nomo::ast::parse(parsed.tokens()).unwrap(); let ast = match nomo::ast::parse(parsed.tokens()) {
Ok(ast) => ast,
Err(err) => {
eprintln!("{}", err.to_report(input));
panic!("Could not evaluate ast");
}
};
insta::assert_debug_snapshot!("2-ast", ast); insta::assert_debug_snapshot!("2-ast", ast);