From d531c96d135f90238b52c71e7047b1ddd73eb31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Thu, 5 Jun 2025 18:44:02 +0200 Subject: [PATCH] parser: recognize/reject prefixed ids and lits in macro calls --- askama_derive/src/tests.rs | 45 +++++++++++-- askama_parser/src/expr.rs | 67 ++++++++++++++++++- askama_parser/src/tests.rs | 16 +++++ ...testcase-minimized-derive-6542584430526464 | 1 + 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-6542584430526464 diff --git a/askama_derive/src/tests.rs b/askama_derive/src/tests.rs index ff4dd3cb..d9d836bf 100644 --- a/askama_derive/src/tests.rs +++ b/askama_derive/src/tests.rs @@ -9,8 +9,8 @@ use proc_macro2::TokenStream; use quote::quote; use similar::{Algorithm, ChangeTag, TextDiffConfig}; -use crate::AnyTemplateArgs; use crate::integration::Buffer; +use crate::{AnyTemplateArgs, derive_template}; #[track_caller] fn build_template(ast: &syn::DeriveInput) -> Result { @@ -1186,7 +1186,7 @@ fn test_generated_with_error() { #[template(ext = "txt", source = "test {#")] struct HelloWorld; }; - let ts = crate::derive_template(ts, import_askama); + let ts = derive_template(ts, import_askama); let _: syn::File = syn::parse2(ts).unwrap(); } @@ -1221,7 +1221,7 @@ fn fuzzed_0b85() -> Result<(), syn::Error> { )] struct a {} }; - let output = crate::derive_template(input, import_askama); + let output = derive_template(input, import_askama); let _: syn::File = syn::parse2(output)?; Ok(()) } @@ -1235,7 +1235,7 @@ fn fuzzed_comparator_chain() -> Result<(), syn::Error> { )] enum fff {} }; - let output = crate::derive_template(input, import_askama); + let output = derive_template(input, import_askama); let _: syn::File = syn::parse2(output)?; Ok(()) } @@ -1293,8 +1293,43 @@ fn test_macro_calls_need_proper_tokens() -> Result<(), syn::Error> { )] struct f {} }; - let output = crate::derive_template(input, import_askama); + let output = derive_template(input, import_askama); assert!(output.to_string().contains("expected valid tokens in macro call")); let _: syn::File = syn::parse2(output)?; Ok(()) } + +#[test] +fn test_macro_call_raw_prefix_without_data() -> Result<(), syn::Error> { + // Regression test for . + // The parser must reject wrong usage of raw prefixes. + let input = quote! { + #[template(ext = "", source = "{{ z!{r#} }}")] + enum q {} + }; + let output = derive_template(input, import_askama); + assert!( + output + .to_string() + .contains("raw prefix `r#` is only allowed with raw identifiers and raw strings") + ); + let _: syn::File = syn::parse2(output)?; + Ok(()) +} + +#[test] +fn test_macro_call_reserved_prefix() -> Result<(), syn::Error> { + // The parser must reject reserved prefixes. + let input = quote! { + #[template(ext = "", source = "{{ z!{hello#world} }}")] + enum q {} + }; + let output = derive_template(input, import_askama); + assert!( + output + .to_string() + .contains("reserved prefix `hello#`, only `r#` is allowed") + ); + let _: syn::File = syn::parse2(output)?; + Ok(()) +} diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs index b3f644b0..e47e29b0 100644 --- a/askama_parser/src/expr.rs +++ b/askama_parser/src/expr.rs @@ -773,8 +773,8 @@ impl<'a> Suffix<'a> { fn token<'a>(i: &mut &'a str) -> ParseResult<'a, Token> { // let some_other = alt(( - // keywords + identifiers - identifier.value(Token::SomeOther), + // keywords + (raw) identifiers + raw strings + identifier_or_prefixed_string, // literals Expr::char.value(Token::SomeOther), Expr::str.value(Token::SomeOther), @@ -783,15 +783,76 @@ impl<'a> Suffix<'a> { ('\'', identifier, not(peek('\''))).value(Token::SomeOther), // punctuations punctuation.value(Token::SomeOther), + hash, )); alt((open.map(Token::Open), close.map(Token::Close), some_other)).parse_next(i) } + fn identifier_or_prefixed_string<'a>(i: &mut &'a str) -> ParseResult<'a, Token> { + let prefix = identifier.parse_next(i)?; + if opt('#').parse_next(i)?.is_none() { + // a simple identifier + return Ok(Token::SomeOther); + } + + match prefix { + // raw cstring or byte slice + "cr" | "br" => {} + // raw string string or identifier + "r" => { + if opt(identifier).parse_next(i)?.is_some() { + return Ok(Token::SomeOther); + } + } + // reserved prefix: reject + _ => { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + format!( + "reserved prefix `{}#`, only `r#` is allowed", + prefix.escape_debug(), + ), + prefix, + ))); + } + } + + let hashes: usize = repeat(.., '#').parse_next(i)?; + if opt('"').parse_next(i)?.is_none() { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + "raw prefix `r#` is only allowed with raw identifiers and raw strings", + prefix, + ))); + } + + let Some((_, j)) = i.split_once(&format!("\"#{:#(i: &mut &'a str) -> ParseResult<'a, Token> { + let start = *i; + '#'.parse_next(i)?; + if opt('"').parse_next(i)?.is_some() { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + "unprefixed guarded string literals are reserved for future use", + start, + ))); + } + Ok(Token::SomeOther) + } + fn punctuation<'a>(i: &mut &'a str) -> ParseResult<'a, ()> { // + // hash '#' omitted let one = one_of([ '+', '-', '*', '/', '%', '^', '!', '&', '|', '=', '>', '<', '@', '_', '.', ',', - ';', ':', '#', '$', '?', '~', + ';', ':', '$', '?', '~', ]); let two = alt(( "&&", "||", "<<", ">>", "+=", "-=", "*=", "/=", "%=", "^=", "&=", "|=", "==", "!=", diff --git a/askama_parser/src/tests.rs b/askama_parser/src/tests.rs index fda99dfe..c641ec48 100644 --- a/askama_parser/src/tests.rs +++ b/askama_parser/src/tests.rs @@ -1417,3 +1417,19 @@ fn comparison_operators_cannot_be_chained() { } } } + +#[test] +fn macro_calls_can_have_raw_prefixes() { + // Related to issue . + let syntax = Syntax::default(); + let inner = r####"r#"test"# r##"test"## r###"test"### r#loop"####; + assert_eq!( + Ast::from_str(&format!("{{{{ z!{{{inner}}} }}}}"), None, &syntax) + .unwrap() + .nodes, + vec![Node::Expr( + Ws(None, None), + WithSpan::no_span(Expr::RustMacro(vec!["z"], inner)), + )], + ); +} diff --git a/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-6542584430526464 b/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-6542584430526464 new file mode 100644 index 00000000..3388ef28 --- /dev/null +++ b/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-6542584430526464 @@ -0,0 +1 @@ +ÿÿÿ{{z!{r#}}} ÿqÿø \ No newline at end of file