From 1f3102163236cb3ff6a42e1c6c3cc8d9b4aa1c22 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 13 Aug 2025 23:11:01 +0200 Subject: [PATCH] Fix wrong macro argument parsing --- askama_parser/src/expr.rs | 32 +++++++++++++++-- ...testcase-minimized-derive-5107711304073216 | 1 + testing/tests/macro.rs | 16 +++++++++ testing/tests/ui/macro-args-hashtag.rs | 34 +++++++++++++++++++ testing/tests/ui/macro-args-hashtag.stderr | 31 +++++++++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5107711304073216 create mode 100644 testing/tests/ui/macro-args-hashtag.rs create mode 100644 testing/tests/ui/macro-args-hashtag.stderr diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs index 5301212d..f8b8d7bc 100644 --- a/askama_parser/src/expr.rs +++ b/askama_parser/src/expr.rs @@ -927,6 +927,35 @@ impl<'a> Suffix<'a> { } } + fn lifetime<'a>(i: &mut InputStream<'a>) -> ParseResult<'a, ()> { + // Before the 2021 edition, we can have whitespace characters between "r#" and the + // identifier so we allow it here. + let start = *i; + '\''.parse_next(i)?; + let Some((is_raw, identifier)) = opt(alt(( + ('r', '#', identifier).map(|(_, _, ident)| (true, ident)), + (identifier, not(peek('#'))).map(|(ident, _)| (false, ident)), + ))) + .parse_next(i)? + else { + return cut_error!("wrong lifetime format", **start); + }; + if !is_raw { + if crate::is_rust_keyword(identifier) { + return cut_error!( + "a non-raw lifetime cannot be named like an existing keyword", + **start, + ); + } + } else if ["Self", "self", "crate", "super", "_"].contains(&identifier) { + return cut_error!(format!("`{identifier}` cannot be a raw lifetime"), **start,); + } + if opt(peek('\'')).parse_next(i)?.is_some() { + return cut_error!("unexpected `'` after lifetime", **start); + } + Ok(()) + } + fn token<'a>(i: &mut InputStream<'a>) -> ParseResult<'a, Token> { // let some_other = alt(( @@ -936,8 +965,7 @@ impl<'a> Suffix<'a> { num_lit.value(Token::SomeOther), // keywords + (raw) identifiers + raw strings identifier_or_prefixed_string.value(Token::SomeOther), - // lifetimes - ('\'', identifier, not(peek('\''))).value(Token::SomeOther), + lifetime.value(Token::SomeOther), // comments line_comment.value(Token::SomeOther), block_comment.value(Token::SomeOther), diff --git a/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5107711304073216 b/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5107711304073216 new file mode 100644 index 00000000..adab7e9c --- /dev/null +++ b/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5107711304073216 @@ -0,0 +1 @@ +ÿÿÿ{{z!{'r#}}}ÿ ÿsÿ \ No newline at end of file diff --git a/testing/tests/macro.rs b/testing/tests/macro.rs index a8192578..c67696a5 100644 --- a/testing/tests/macro.rs +++ b/testing/tests/macro.rs @@ -630,3 +630,19 @@ fn test_macro_caller_is_defined_check() { "no caller defined|this time with caller" ); } + +// This test ensures that raw lifetimes are correctly handled. +#[test] +fn test_macro_raw_lifetime() { + macro_rules! test { + ('r#ignore_me) => { + "ok" + }; + } + + #[derive(Template)] + #[template(source = r##"{{ test!('r#ignore_me) }}"##, ext = "txt")] + struct Foo; + + assert_eq!(Foo.render().unwrap(), "ok"); +} diff --git a/testing/tests/ui/macro-args-hashtag.rs b/testing/tests/ui/macro-args-hashtag.rs new file mode 100644 index 00000000..d12d0c9f --- /dev/null +++ b/testing/tests/ui/macro-args-hashtag.rs @@ -0,0 +1,34 @@ +// This test ensures that we have the right error if a `#` character isn't prepended by +// a whitespace character and also that lifetimes are correctly handled. + +use askama::Template; + +#[derive(Template)] +#[template( + source = r###"{{z!{'r#}}}"###, + ext = "html" +)] +struct Example; + +#[derive(Template)] +#[template( + source = r###"{{z!{'r# y}}}"###, + ext = "html" +)] +struct Example2; + +#[derive(Template)] +#[template( + source = r###"{{z!{'break}}}"###, + ext = "html" +)] +struct Example3; + +#[derive(Template)] +#[template( + source = r###"{{z!{'r#self}}}"###, + ext = "html" +)] +struct Example4; + +fn main() {} diff --git a/testing/tests/ui/macro-args-hashtag.stderr b/testing/tests/ui/macro-args-hashtag.stderr new file mode 100644 index 00000000..5675bc89 --- /dev/null +++ b/testing/tests/ui/macro-args-hashtag.stderr @@ -0,0 +1,31 @@ +error: wrong lifetime format + --> :1:5 + "'r#}}}" + --> tests/ui/macro-args-hashtag.rs:8:14 + | +8 | source = r###"{{z!{'r#}}}"###, + | ^^^^^^^^^^^^^^^^^^^^ + +error: wrong lifetime format + --> :1:5 + "'r# y}}}" + --> tests/ui/macro-args-hashtag.rs:15:14 + | +15 | source = r###"{{z!{'r# y}}}"###, + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: a non-raw lifetime cannot be named like an existing keyword + --> :1:5 + "'break}}}" + --> tests/ui/macro-args-hashtag.rs:22:14 + | +22 | source = r###"{{z!{'break}}}"###, + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: `self` cannot be a raw lifetime + --> :1:5 + "'r#self}}}" + --> tests/ui/macro-args-hashtag.rs:29:14 + | +29 | source = r###"{{z!{'r#self}}}"###, + | ^^^^^^^^^^^^^^^^^^^^^^^^