diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index a4a9fa6e..3054be4b 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -394,6 +394,7 @@ fn compile_time_escape<'a>(expr: &Expr<'a>, escaper: &str) -> Option { if content.find('\\').is_none() { // if the literal does not contain any backslashes, then it does not need unescaping diff --git a/askama_derive/src/generator/filter.rs b/askama_derive/src/generator/filter.rs index 332eb8a6..d5317e6e 100644 --- a/askama_derive/src/generator/filter.rs +++ b/askama_derive/src/generator/filter.rs @@ -275,11 +275,19 @@ impl<'a> Generator<'a, '_> { &WithSpan::new_without_span(Expr::StrLit(StrLit { prefix: None, content: "", + contains_null: false, + contains_unicode_character: false, + contains_unicode_escape: false, + contains_high_ascii: false, })); const PLURAL: &WithSpan<'static, Expr<'static>> = &WithSpan::new_without_span(Expr::StrLit(StrLit { prefix: None, content: "s", + contains_null: false, + contains_unicode_character: false, + contains_unicode_escape: false, + contains_high_ascii: false, })); const ARGUMENTS: &[&FilterArgument; 3] = &[ FILTER_SOURCE, @@ -494,7 +502,10 @@ impl<'a> Generator<'a, '_> { let [source, opt_escaper] = collect_filter_args(ctx, "escape", node, args, ARGUMENTS)?; let opt_escaper = if !is_argument_placeholder(opt_escaper) { - let Expr::StrLit(StrLit { prefix, content }) = **opt_escaper else { + let Expr::StrLit(StrLit { + prefix, content, .. + }) = **opt_escaper + else { return Err(ctx.generate_error("invalid escaper type for escape filter", node)); }; if let Some(prefix) = prefix { diff --git a/askama_derive/src/tests.rs b/askama_derive/src/tests.rs index d9d836bf..f1829715 100644 --- a/askama_derive/src/tests.rs +++ b/askama_derive/src/tests.rs @@ -1278,12 +1278,12 @@ fn test_macro_names_that_need_escaping() { } #[test] -#[rustfmt::skip] // FIXME: rustfmt bug fn test_macro_calls_need_proper_tokens() -> Result<(), syn::Error> { // Regression test for fuzzed error . // Macro calls can contains any valid tokens, but only valid tokens. // Invalid tokens will be rejected by rust, so we must not emit them. + #[rustfmt::skip] // FIXME: rustfmt bug let input = quote! { #[template( ext = "", @@ -1294,7 +1294,11 @@ fn test_macro_calls_need_proper_tokens() -> Result<(), syn::Error> { struct f {} }; let output = derive_template(input, import_askama); - assert!(output.to_string().contains("expected valid tokens in macro call")); + assert!( + output + .to_string() + .contains("expected valid tokens in macro call") + ); let _: syn::File = syn::parse2(output)?; Ok(()) } @@ -1311,7 +1315,7 @@ fn test_macro_call_raw_prefix_without_data() -> Result<(), syn::Error> { assert!( output .to_string() - .contains("raw prefix `r#` is only allowed with raw identifiers and raw strings") + .contains("prefix `r#` is only allowed with raw identifiers and raw strings") ); let _: syn::File = syn::parse2(output)?; Ok(()) @@ -1325,10 +1329,27 @@ fn test_macro_call_reserved_prefix() -> Result<(), syn::Error> { enum q {} }; let output = derive_template(input, import_askama); + assert!(output.to_string().contains("reserved prefix `hello#`")); + let _: syn::File = syn::parse2(output)?; + Ok(()) +} + +#[test] +fn test_macro_call_valid_raw_cstring() -> Result<(), syn::Error> { + // Regression test for . + // CString literals must not contain NULs. + + #[rustfmt::skip] // FIXME: rustfmt bug + let input = quote! { + #[template(ext = "", source = "{{ c\"\0\" }}")] +// ^^ NUL is not allowed in cstring literals + enum l {} + }; + let output = derive_template(input, import_askama); assert!( output .to_string() - .contains("reserved prefix `hello#`, only `r#` is allowed") + .contains("null characters in C string literals are not supported") ); let _: syn::File = syn::parse2(output)?; Ok(()) diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs index e47e29b0..ca3e7ec9 100644 --- a/askama_parser/src/expr.rs +++ b/askama_parser/src/expr.rs @@ -7,13 +7,12 @@ use winnow::combinator::{ alt, cut_err, fail, not, opt, peek, preceded, repeat, separated, terminated, }; use winnow::error::ParserError as _; -use winnow::token::one_of; use crate::node::CondTest; use crate::{ CharLit, ErrorContext, Level, Num, ParseErr, ParseResult, PathOrIdentifier, Span, StrLit, - WithSpan, char_lit, filter, identifier, keyword, num_lit, path_or_identifier, skip_ws0, - skip_ws1, str_lit, ws, + StrPrefix, WithSpan, char_lit, filter, identifier, keyword, num_lit, path_or_identifier, + skip_ws0, skip_ws1, str_lit, ws, }; macro_rules! expr_prec_layer { @@ -773,12 +772,12 @@ impl<'a> Suffix<'a> { fn token<'a>(i: &mut &'a str) -> ParseResult<'a, Token> { // let some_other = alt(( - // keywords + (raw) identifiers + raw strings - identifier_or_prefixed_string, // literals Expr::char.value(Token::SomeOther), Expr::str.value(Token::SomeOther), Expr::num.value(Token::SomeOther), + // keywords + (raw) identifiers + raw strings + identifier_or_prefixed_string.value(Token::SomeOther), // lifetimes ('\'', identifier, not(peek('\''))).value(Token::SomeOther), // punctuations @@ -788,51 +787,96 @@ impl<'a> Suffix<'a> { 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> { + fn identifier_or_prefixed_string<'a>(i: &mut &'a str) -> ParseResult<'a, ()> { + // + let prefix = identifier.parse_next(i)?; - if opt('#').parse_next(i)?.is_none() { - // a simple identifier - return Ok(Token::SomeOther); + let hashes: usize = repeat(.., '#').parse_next(i)?; + if hashes >= 256 { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + "a maximum of 255 hashes `#` are allowed with raw strings", + prefix, + ))); } - match prefix { + let str_kind = match prefix { // raw cstring or byte slice - "cr" | "br" => {} + "br" => Some(StrPrefix::Binary), + "cr" => Some(StrPrefix::CLike), // raw string string or identifier - "r" => { - if opt(identifier).parse_next(i)?.is_some() { - return Ok(Token::SomeOther); - } - } + "r" => None, + // a simple identifier + _ if hashes == 0 => return Ok(()), // reserved prefix: reject _ => { return Err(winnow::error::ErrMode::Cut(ErrorContext::new( - format!( - "reserved prefix `{}#`, only `r#` is allowed", - prefix.escape_debug(), - ), + format!("reserved prefix `{}#`", 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!("\"#{:# inner + .bytes() + .any(|b| !b.is_ascii()) + .then_some("binary string literals must not contain non-ASCII characters"), + Some(StrPrefix::CLike) => inner + .bytes() + .any(|b| b == 0) + .then_some("cstring literals must not contain NUL characters"), + None => None, + }; + if let Some(msg) = msg { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new(msg, prefix))); + } + + Ok(()) + } else if hashes == 0 { + // a simple identifier + Ok(()) + } else if opt(identifier).parse_next(i)?.is_some() { + // got a raw identifier + + if str_kind.is_some() { + // an invalid raw identifier like `cr#async` + Err(winnow::error::ErrMode::Cut(ErrorContext::new( + format!( + "reserved prefix `{}#`, only `r#` is allowed with raw identifiers", + prefix.escape_debug(), + ), + prefix, + ))) + } else if hashes > 1 { + // an invalid raw identifier like `r##async` + Err(winnow::error::ErrMode::Cut(ErrorContext::new( + "only one `#` is allowed in raw identifier delimitation", + prefix, + ))) + } else { + // a raw identifier like `r#async` + Ok(()) + } + } else { + Err(winnow::error::ErrMode::Cut(ErrorContext::new( + format!( + "prefix `{}#` is only allowed with raw identifiers and raw strings", + prefix.escape_debug(), + ), + prefix, + ))) + } } fn hash<'a>(i: &mut &'a str) -> ParseResult<'a, Token> { @@ -850,16 +894,36 @@ impl<'a> Suffix<'a> { fn punctuation<'a>(i: &mut &'a str) -> ParseResult<'a, ()> { // // hash '#' omitted - let one = one_of([ - '+', '-', '*', '/', '%', '^', '!', '&', '|', '=', '>', '<', '@', '_', '.', ',', - ';', ':', '$', '?', '~', - ]); - let two = alt(( + + const ONE_CHAR: &[u8] = b"+-*/%^!&|=><@_.,;:$?~"; + const TWO_CHARS: &[&str] = &[ "&&", "||", "<<", ">>", "+=", "-=", "*=", "/=", "%=", "^=", "&=", "|=", "==", "!=", ">=", "<=", "..", "::", "->", "=>", "<-", - )); - let three = alt(("<<=", ">>=", "...", "..=")); - alt((three.value(()), two.value(()), one.value(()))).parse_next(i) + ]; + const THREE_CHARS: &[&str] = &["<<=", ">>=", "...", "..="]; + + // need to check long to short + if let Some((head, tail)) = i.split_at_checked(3) { + if THREE_CHARS.contains(&head) { + *i = tail; + return Ok(()); + } + } + if let Some((head, tail)) = i.split_at_checked(2) { + if TWO_CHARS.contains(&head) { + *i = tail; + return Ok(()); + } + } + if let Some((head, tail)) = i.split_at_checked(1) { + if let [head] = head.as_bytes() { + if ONE_CHAR.contains(head) { + *i = tail; + return Ok(()); + } + } + } + fail(i) } fn open<'a>(i: &mut &'a str) -> ParseResult<'a, Group> { diff --git a/askama_parser/src/lib.rs b/askama_parser/src/lib.rs index 6f5644de..f9bfee35 100644 --- a/askama_parser/src/lib.rs +++ b/askama_parser/src/lib.rs @@ -19,10 +19,12 @@ use std::sync::Arc; use std::{fmt, str}; use winnow::ascii::take_escaped; -use winnow::combinator::{alt, cut_err, delimited, fail, not, opt, peek, preceded, repeat}; +use winnow::combinator::{ + alt, cut_err, delimited, fail, not, opt, peek, preceded, repeat, terminated, +}; use winnow::error::FromExternalError; -use winnow::stream::Stream as _; -use winnow::token::{any, one_of, take_till, take_while}; +use winnow::stream::{AsChar, Stream as _}; +use winnow::token::{any, none_of, one_of, take_till, take_while}; use winnow::{ModalParser, Parser}; use crate::ascii_str::{AsciiChar, AsciiStr}; @@ -544,28 +546,166 @@ impl fmt::Display for StrPrefix { #[derive(Clone, Debug, PartialEq)] pub struct StrLit<'a> { - pub prefix: Option, + /// the unparsed (but validated) content pub content: &'a str, -} - -fn str_lit_without_prefix<'a>(i: &mut &'a str) -> ParseResult<'a> { - let s = delimited( - '"', - opt(take_escaped(take_till(1.., ['\\', '"']), '\\', any)), - '"', - ) - .parse_next(i)?; - Ok(s.unwrap_or_default()) + /// whether the string literal is unprefixed, a cstring or binary slice + pub prefix: Option, + /// contains a NUL character, either escaped `'\0'` or the very characters; + /// not allowed in cstring literals + pub contains_null: bool, + /// contains a non-ASCII character, either as `\u{123456}` or as an unescaped character; + /// not allowed in binary slices + pub contains_unicode_character: bool, + /// contains unicode escape sequences like `\u{12}` (regardless of its range); + /// not allowed in binary slices + pub contains_unicode_escape: bool, + /// contains a non-ASCII range escape sequence like `\x80`; + /// not allowed in unprefix strings + pub contains_high_ascii: bool, } fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> { - let (prefix, content) = (opt(alt(('b', 'c'))), str_lit_without_prefix).parse_next(i)?; - let prefix = match prefix { - Some('b') => Some(StrPrefix::Binary), - Some('c') => Some(StrPrefix::CLike), - _ => None, + // + + fn inner<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Sequence<'a> { + Text(&'a str), + Close, + Escape, + } + + let start = *i; + let mut contains_null = false; + let mut contains_unicode_character = false; + let mut contains_unicode_escape = false; + let mut contains_high_ascii = false; + + while !i.is_empty() { + let seq = alt(( + repeat::<_, _, (), _, _>(1.., none_of(['\\', '"'])) + .take() + .map(Sequence::Text), + '\\'.value(Sequence::Escape), + peek('"').value(Sequence::Close), + )) + .parse_next(i)?; + + match seq { + Sequence::Text(s) => { + contains_unicode_character = + contains_unicode_character || s.bytes().any(|c: u8| !c.is_ascii()); + contains_null = contains_null || s.bytes().any(|c: u8| c == 0); + continue; + } + Sequence::Close => break, + Sequence::Escape => {} + } + + match any.parse_next(i)? { + '\'' | '"' | 'n' | 'r' | 't' | '\\' => continue, + '0' => { + contains_null = true; + continue; + } + 'x' => { + let code = take_while(2, AsChar::is_hex_digit).parse_next(i)?; + match u8::from_str_radix(code, 16).unwrap() { + 0 => contains_null = true, + 128.. => contains_high_ascii = true, + _ => {} + } + } + 'u' => { + contains_unicode_escape = true; + let code = delimited('{', take_while(1..6, AsChar::is_hex_digit), '}') + .parse_next(i)?; + match u32::from_str_radix(code, 16).unwrap() { + 0 => contains_null = true, + 0xd800..0xe000 => { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + "unicode escape must not be a surrogate", + start, + ))); + } + 128.. => contains_unicode_character = true, + _ => {} + } + } + _ => return fail(i), + } + } + + Ok(StrLit { + content: "", + prefix: None, + contains_null, + contains_unicode_character, + contains_unicode_escape, + contains_high_ascii, + }) + } + + let start = *i; + + let prefix = terminated( + opt(alt(( + 'b'.value(StrPrefix::Binary), + 'c'.value(StrPrefix::CLike), + ))), + '"', + ) + .parse_next(i)?; + + let lit = opt(terminated(inner.with_taken(), '"')).parse_next(i)?; + let Some((mut lit, content)) = lit else { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + "unclosed or broken string", + start, + ))); }; - Ok(StrLit { prefix, content }) + lit.content = content; + lit.prefix = prefix; + + let msg = match prefix { + Some(StrPrefix::Binary) => { + if lit.contains_unicode_character { + Some("non-ASCII character in byte string literal") + } else if lit.contains_unicode_escape { + Some("unicode escape in byte string") + } else { + None + } + } + Some(StrPrefix::CLike) => lit + .contains_null + .then_some("null characters in C string literals are not supported"), + None => lit.contains_high_ascii.then_some("out of range hex escape"), + }; + if let Some(msg) = msg { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new(msg, start))); + } + + Ok(lit) +} + +fn str_lit_without_prefix<'a>(i: &mut &'a str) -> ParseResult<'a> { + let start = *i; + let lit = str_lit.parse_next(i)?; + + let kind = match lit.prefix { + Some(StrPrefix::Binary) => Some("binary slice"), + Some(StrPrefix::CLike) => Some("cstring"), + None => None, + }; + if let Some(kind) = kind { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + format!("expected an unprefixed normal string, not a {kind}"), + start, + ))); + } + + Ok(lit.content) } #[derive(Clone, Copy, Debug, PartialEq)] @@ -1478,7 +1618,11 @@ mod test { "", StrLit { prefix: Some(StrPrefix::Binary), - content: "hello" + content: "hello", + contains_null: false, + contains_unicode_character: false, + contains_unicode_escape: false, + contains_high_ascii: false, } ) ); @@ -1488,7 +1632,11 @@ mod test { "", StrLit { prefix: Some(StrPrefix::CLike), - content: "hello" + content: "hello", + contains_null: false, + contains_unicode_character: false, + contains_unicode_escape: false, + contains_high_ascii: false, } ) ); diff --git a/askama_parser/src/tests.rs b/askama_parser/src/tests.rs index c641ec48..65ecf506 100644 --- a/askama_parser/src/tests.rs +++ b/askama_parser/src/tests.rs @@ -237,6 +237,10 @@ fn test_parse_var_call() { WithSpan::no_span(Expr::StrLit(StrLit { content: "123", prefix: None, + contains_null: false, + contains_unicode_character: false, + contains_unicode_escape: false, + contains_high_ascii: false, })), WithSpan::no_span(int_lit("3")) ], @@ -283,6 +287,10 @@ fn test_parse_path_call() { WithSpan::no_span(Expr::StrLit(StrLit { content: "123", prefix: None, + contains_null: false, + contains_unicode_character: false, + contains_unicode_escape: false, + contains_high_ascii: false, })), WithSpan::no_span(int_lit("3")) ], diff --git a/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5611392537526272 b/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5611392537526272 new file mode 100644 index 00000000..ed816aa5 Binary files /dev/null and b/fuzzing/fuzz/artifacts/derive/clusterfuzz-testcase-minimized-derive-5611392537526272 differ diff --git a/testing/templates/macro-call-raw-string-many-hashes.html b/testing/templates/macro-call-raw-string-many-hashes.html new file mode 100644 index 00000000..2a76c6fe --- /dev/null +++ b/testing/templates/macro-call-raw-string-many-hashes.html @@ -0,0 +1 @@ +{{ z!(hello################################################################################################################################################################################################################################################################world) }} diff --git a/testing/tests/ui/comparator-chaining.rs b/testing/tests/ui/comparator-chaining.rs index 0edf4aac..eb486a3b 100644 --- a/testing/tests/ui/comparator-chaining.rs +++ b/testing/tests/ui/comparator-chaining.rs @@ -42,7 +42,7 @@ struct ThreeTimesOk2 { #[derive(Template)] #[template( ext = "", - source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333 :1:52 - " tests/ui/comparator-chaining.rs:45:14 | -45 | source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333 :1:3 + "\"hello world }}" + --> tests/ui/illegal-string-literals.rs:6:34 + | +6 | #[template(ext = "txt", source = r#"{{ "hello world }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: unclosed or broken string + --> :1:3 + "\"hello world\\\" }}" + --> tests/ui/illegal-string-literals.rs:10:34 + | +10 | #[template(ext = "txt", source = r#"{{ "hello world\" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unclosed or broken string + --> :1:3 + "b\"hello world }}" + --> tests/ui/illegal-string-literals.rs:14:34 + | +14 | #[template(ext = "txt", source = r#"{{ b"hello world }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unclosed or broken string + --> :1:3 + "b\"hello world\\\" }}" + --> tests/ui/illegal-string-literals.rs:18:34 + | +18 | #[template(ext = "txt", source = r#"{{ b"hello world\" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unclosed or broken string + --> :1:3 + "c\"hello world }}" + --> tests/ui/illegal-string-literals.rs:22:34 + | +22 | #[template(ext = "txt", source = r#"{{ c"hello world }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unclosed or broken string + --> :1:3 + "c\"hello world\\\" }}" + --> tests/ui/illegal-string-literals.rs:26:34 + | +26 | #[template(ext = "txt", source = r#"{{ c"hello world\" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: out of range hex escape + --> :1:3 + "\"hello \\x80 world\" }}" + --> tests/ui/illegal-string-literals.rs:32:34 + | +32 | #[template(ext = "txt", source = r#"{{ "hello \x80 world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: out of range hex escape + --> :1:3 + "\"hello \\xff world\" }}" + --> tests/ui/illegal-string-literals.rs:36:34 + | +36 | #[template(ext = "txt", source = r#"{{ "hello \xff world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unclosed or broken string + --> :1:3 + "\"hello \\u{128521} world\" }}" + --> tests/ui/illegal-string-literals.rs:64:34 + | +64 | #[template(ext = "txt", source = r#"{{ "hello \u{128521} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:4 + "hello \\u{d83d}\\u{de09} world\" }}" + --> tests/ui/illegal-string-literals.rs:70:34 + | +70 | #[template(ext = "txt", source = r#"{{ "hello \u{d83d}\u{de09} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:4 + "hello \\u{d83d} world\" }}" + --> tests/ui/illegal-string-literals.rs:74:34 + | +74 | #[template(ext = "txt", source = r#"{{ "hello \u{d83d} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:4 + "hello \\u{de09} world\" }}" + --> tests/ui/illegal-string-literals.rs:78:34 + | +78 | #[template(ext = "txt", source = r#"{{ "hello \u{de09} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:5 + "hello \\u{d83d}\\u{de09} world\" }}" + --> tests/ui/illegal-string-literals.rs:82:34 + | +82 | #[template(ext = "txt", source = r#"{{ b"hello \u{d83d}\u{de09} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:5 + "hello \\u{d83d} world\" }}" + --> tests/ui/illegal-string-literals.rs:86:34 + | +86 | #[template(ext = "txt", source = r#"{{ b"hello \u{d83d} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:5 + "hello \\u{de09} world\" }}" + --> tests/ui/illegal-string-literals.rs:90:34 + | +90 | #[template(ext = "txt", source = r#"{{ b"hello \u{de09} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:5 + "hello \\u{d83d}\\u{de09} world\" }}" + --> tests/ui/illegal-string-literals.rs:94:34 + | +94 | #[template(ext = "txt", source = r#"{{ c"hello \u{d83d}\u{de09} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:5 + "hello \\u{d83d} world\" }}" + --> tests/ui/illegal-string-literals.rs:98:34 + | +98 | #[template(ext = "txt", source = r#"{{ c"hello \u{d83d} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unicode escape must not be a surrogate + --> :1:5 + "hello \\u{de09} world\" }}" + --> tests/ui/illegal-string-literals.rs:102:34 + | +102 | #[template(ext = "txt", source = r#"{{ c"hello \u{de09} world" }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/testing/tests/ui/raw-prefix.rs b/testing/tests/ui/raw-prefix.rs new file mode 100644 index 00000000..8c7388fb --- /dev/null +++ b/testing/tests/ui/raw-prefix.rs @@ -0,0 +1,108 @@ +// Regression test for . + +use askama::Template; + +// Cstring literals must not contain NULs. + +#[derive(Template)] +#[template( + source = "{{ z!(cr#\"\0\"#) }}", + ext = "txt" +)] +struct MacroCallRawCstring1; + +#[derive(Template)] +#[template( + source = "{{ z!(cr##\"\0\"##) }}", + ext = "txt" +)] +struct MacroCallRawCstring2; + +#[derive(Template)] +#[template( + source = "{{ z!(cr###\"\0\"###) }}", + ext = "txt" +)] +struct MacroCallRawCstring3; + +// Binary string literals must not contain NULs. + +#[derive(Template)] +#[template( + source = "{{ z!(br#\"πŸ˜Άβ€πŸŒ«οΈ\"#) }}", + ext = "txt" +)] +struct MacroCallRawBinaryString1; + +#[derive(Template)] +#[template( + source = "{{ z!(br##\"πŸ˜Άβ€πŸŒ«οΈ\"##) }}", + ext = "txt" +)] +struct MacroCallRawBinaryString2; + +#[derive(Template)] +#[template( + source = "{{ z!(br###\"πŸ˜Άβ€πŸŒ«οΈ\"###) }}", + ext = "txt" +)] +struct MacroCallRawBinaryString3; + +// Only `r#` is allowed as prefix idenfiers + +#[derive(Template)] +#[template( + source = "{{ z!(br#async) }}", + ext = "txt" +)] +struct MacroCallIllegalPrefix1; + +#[derive(Template)] +#[template( + source = "{{ z!(cr#async) }}", + ext = "txt" +)] +struct MacroCallIllegalPrefix2; + +#[derive(Template)] +#[template( + source = "{{ z!(r##async) }}", + ext = "txt" +)] +struct MacroCallIllegalPrefix3; + +#[derive(Template)] +#[template( + source = "{{ z!(br##async) }}", + ext = "txt" +)] +struct MacroCallIllegalPrefix4; + +#[derive(Template)] +#[template( + source = "{{ z!(cr##async) }}", + ext = "txt" +)] +struct MacroCallIllegalPrefix5; + +#[derive(Template)] +#[template( + source = "{{ z!(hello#world) }}", + ext = "txt" +)] +struct MacroCallReservedPrefix1; + +#[derive(Template)] +#[template( + source = "{{ z!(hello##world) }}", + ext = "txt" +)] +struct MacroCallReservedPrefix2; + +// No more than 255 hashes + +#[derive(Template)] +#[template(path = "macro-call-raw-string-many-hashes.html")] +struct MacroCallManyHashes; + +fn main() {} diff --git a/testing/tests/ui/raw-prefix.stderr b/testing/tests/ui/raw-prefix.stderr new file mode 100644 index 00000000..929a33b3 --- /dev/null +++ b/testing/tests/ui/raw-prefix.stderr @@ -0,0 +1,111 @@ +error: cstring literals must not contain NUL characters + --> :1:6 + "cr#\"\0\"#) }}" + --> tests/ui/raw-prefix.rs:9:14 + | +9 | source = "{{ z!(cr#\"\0\"#) }}", + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: cstring literals must not contain NUL characters + --> :1:6 + "cr##\"\0\"##) }}" + --> tests/ui/raw-prefix.rs:16:14 + | +16 | source = "{{ z!(cr##\"\0\"##) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: cstring literals must not contain NUL characters + --> :1:6 + "cr###\"\0\"###) }}" + --> tests/ui/raw-prefix.rs:23:14 + | +23 | source = "{{ z!(cr###\"\0\"###) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: binary string literals must not contain non-ASCII characters + --> :1:6 + "br#\"😢\u{200d}🌫\u{fe0f}\"#) }}" + --> tests/ui/raw-prefix.rs:32:14 + | +32 | source = "{{ z!(br#\"😢🌫️\"#) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: binary string literals must not contain non-ASCII characters + --> :1:6 + "br##\"😢\u{200d}🌫\u{fe0f}\"##) }}" + --> tests/ui/raw-prefix.rs:39:14 + | +39 | source = "{{ z!(br##\"😢🌫️\"##) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: binary string literals must not contain non-ASCII characters + --> :1:6 + "br###\"😢\u{200d}🌫\u{fe0f}\"###) }}" + --> tests/ui/raw-prefix.rs:46:14 + | +46 | source = "{{ z!(br###\"😢🌫️\"###) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: reserved prefix `br#`, only `r#` is allowed with raw identifiers + --> :1:6 + "br#async) }}" + --> tests/ui/raw-prefix.rs:55:14 + | +55 | source = "{{ z!(br#async) }}", + | ^^^^^^^^^^^^^^^^^^^^ + +error: reserved prefix `cr#`, only `r#` is allowed with raw identifiers + --> :1:6 + "cr#async) }}" + --> tests/ui/raw-prefix.rs:62:14 + | +62 | source = "{{ z!(cr#async) }}", + | ^^^^^^^^^^^^^^^^^^^^ + +error: only one `#` is allowed in raw identifier delimitation + --> :1:6 + "r##async) }}" + --> tests/ui/raw-prefix.rs:69:14 + | +69 | source = "{{ z!(r##async) }}", + | ^^^^^^^^^^^^^^^^^^^^ + +error: reserved prefix `br#`, only `r#` is allowed with raw identifiers + --> :1:6 + "br##async) }}" + --> tests/ui/raw-prefix.rs:76:14 + | +76 | source = "{{ z!(br##async) }}", + | ^^^^^^^^^^^^^^^^^^^^^ + +error: reserved prefix `cr#`, only `r#` is allowed with raw identifiers + --> :1:6 + "cr##async) }}" + --> tests/ui/raw-prefix.rs:83:14 + | +83 | source = "{{ z!(cr##async) }}", + | ^^^^^^^^^^^^^^^^^^^^^ + +error: reserved prefix `hello#` + --> :1:6 + "hello#world) }}" + --> tests/ui/raw-prefix.rs:90:14 + | +90 | source = "{{ z!(hello#world) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: reserved prefix `hello#` + --> :1:6 + "hello##world) }}" + --> tests/ui/raw-prefix.rs:97:14 + | +97 | source = "{{ z!(hello##world) }}", + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: a maximum of 255 hashes `#` are allowed with raw strings + --> testing/templates/macro-call-raw-string-many-hashes.html:1:6 + "hello###########################################################################"... + --> tests/ui/raw-prefix.rs:105:19 + | +105 | #[template(path = "macro-call-raw-string-many-hashes.html")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^