Merge pull request #479 from Kijewski/issue-425

parser: reject illegal string literals
This commit is contained in:
Guillaume Gomez 2025-06-10 19:55:32 +02:00 committed by GitHub
commit 9309acaa01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 797 additions and 76 deletions

View File

@ -394,6 +394,7 @@ fn compile_time_escape<'a>(expr: &Expr<'a>, escaper: &str) -> Option<Writable<'a
Expr::StrLit(StrLit {
prefix: None,
content,
..
}) => {
if content.find('\\').is_none() {
// if the literal does not contain any backslashes, then it does not need unescaping

View File

@ -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 {

View File

@ -1278,12 +1278,12 @@ fn test_macro_names_that_need_escaping() {
}
#[test]
#[rustfmt::skip] // FIXME: rustfmt bug <https://github.com/rust-lang/rustfmt/issues/6565>
fn test_macro_calls_need_proper_tokens() -> Result<(), syn::Error> {
// Regression test for fuzzed error <https://github.com/askama-rs/askama/issues/459>.
// 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 <https://github.com/rust-lang/rustfmt/issues/5489>
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 <https://github.com/askama-rs/askama/issues/478>.
// CString literals must not contain NULs.
#[rustfmt::skip] // FIXME: rustfmt bug <https://github.com/rust-lang/rustfmt/issues/5489>
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(())

View File

@ -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> {
// <https://doc.rust-lang.org/reference/tokens.html>
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, ()> {
// <https://doc.rust-lang.org/reference/tokens.html#r-lex.token.literal.str-raw.syntax>
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!("\"#{:#<hashes$}", "")) else {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(
"unterminated raw string",
prefix,
)));
};
*i = j;
Ok(Token::SomeOther)
if opt('"').parse_next(i)?.is_some() {
// got a raw string
let Some((inner, j)) = i.split_once(&format!("\"{:#<hashes$}", "")) else {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(
"unterminated raw string",
prefix,
)));
};
*i = j;
let msg = match str_kind {
Some(StrPrefix::Binary) => 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, ()> {
// <https://doc.rust-lang.org/reference/tokens.html#punctuation>
// 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> {

View File

@ -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<StrPrefix>,
/// 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<StrPrefix>,
/// 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,
// <https://doc.rust-lang.org/reference/tokens.html#r-lex.token.literal.str.syntax>
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,
}
)
);

View File

@ -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"))
],

View File

@ -0,0 +1 @@
{{ z!(hello################################################################################################################################################################################################################################################################world) }}

View File

@ -42,7 +42,7 @@ struct ThreeTimesOk2 {
#[derive(Template)]
#[template(
ext = "",
source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333<c\"}\u{1}2}\0\"<c7}}2\"\"\"\"\0\0\0\0"
source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333<c\"}\u{1}2}\"<c7}}2\"\"\"\"\0\0\0\0"
)]
struct Regression {}

View File

@ -32,8 +32,8 @@ error: comparison operators cannot be chained; consider using explicit parenthes
error: comparison operators cannot be chained; consider using explicit parentheses, e.g. `(_ < _) < _`
--> <source attribute>:1:52
"<c\"}\u{1}2}\0\"<c7}}2\"\"\"\"\0\0\0\0"
"<c\"}\u{1}2}\"<c7}}2\"\"\"\"\0\0\0\0"
--> tests/ui/comparator-chaining.rs:45:14
|
45 | source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333<c\"}\u{1}2}\0\"<c7}}2\"\"\"\"\0\0\0\0"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45 | source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333<c\"}\u{1}2}\"<c7}}2\"\"\"\"\0\0\0\0"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,105 @@
use askama::Template;
// Strings must be closed
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello world }}"#)]
struct Unclosed1;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello world\" }}"#)]
struct Unclosed2;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ b"hello world }}"#)]
struct Unclosed3;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ b"hello world\" }}"#)]
struct Unclosed4;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ c"hello world }}"#)]
struct Unclosed5;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ c"hello world\" }}"#)]
struct Unclosed6;
// Unprefix string literals must not contain hex escapes sequences higher than `\x7f`
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \x80 world" }}"#)]
struct HighAscii1;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \xff world" }}"#)]
struct HighAscii2;
// Cstring literals must not contain null characters
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \0 world" }}"#)]
struct NulEscapeSequence1;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \x00 world" }}"#)]
struct NulEscapeSequence2;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \u{0} world" }}"#)]
struct NulEscapeSequence3;
#[derive(Template)]
#[template(ext = "txt", source = "{{ \"hello \0 world\" }}")]
struct NulCharacter;
// Binary slice literals must not contain non-ASCII characters
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello 😉 world" }}"#)]
struct UnicodeCharacter;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \u{128521} world" }}"#)]
struct UnicodeEscapeSequence;
// Surrogate characters (even if paired) are not allowed at all
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \u{d83d}\u{de09} world" }}"#)]
struct SurrogatePaired1;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \u{d83d} world" }}"#)]
struct SurrogateLow1;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ "hello \u{de09} world" }}"#)]
struct SurrogateHigh1;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ b"hello \u{d83d}\u{de09} world" }}"#)]
struct SurrogatePaired2;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ b"hello \u{d83d} world" }}"#)]
struct SurrogateLow2;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ b"hello \u{de09} world" }}"#)]
struct SurrogateHigh2;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ c"hello \u{d83d}\u{de09} world" }}"#)]
struct SurrogatePaired3;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ c"hello \u{d83d} world" }}"#)]
struct SurrogateLow3;
#[derive(Template)]
#[template(ext = "txt", source = r#"{{ c"hello \u{de09} world" }}"#)]
struct SurrogateHigh3;
fn main() {}

View File

@ -0,0 +1,143 @@
error: unclosed or broken string
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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" }}"#)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,108 @@
// Regression test for <https://github.com/askama-rs/askama/issues/478>.
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() {}

View File

@ -0,0 +1,111 @@
error: cstring literals must not contain NUL characters
--> <source attribute>:1:6
"cr#\"\0\"#) }}"
--> tests/ui/raw-prefix.rs:9:14
|
9 | source = "{{ z!(cr#\"\0\"#) }}",
| ^^^^^^^^^^^^^^^^^^^^^^
error: cstring literals must not contain NUL characters
--> <source attribute>:1:6
"cr##\"\0\"##) }}"
--> tests/ui/raw-prefix.rs:16:14
|
16 | source = "{{ z!(cr##\"\0\"##) }}",
| ^^^^^^^^^^^^^^^^^^^^^^^^
error: cstring literals must not contain NUL characters
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>: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
--> <source attribute>:1:6
"cr##async) }}"
--> tests/ui/raw-prefix.rs:83:14
|
83 | source = "{{ z!(cr##async) }}",
| ^^^^^^^^^^^^^^^^^^^^^
error: reserved prefix `hello#`
--> <source attribute>:1:6
"hello#world) }}"
--> tests/ui/raw-prefix.rs:90:14
|
90 | source = "{{ z!(hello#world) }}",
| ^^^^^^^^^^^^^^^^^^^^^^^
error: reserved prefix `hello#`
--> <source attribute>: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")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^