Skip to content

Commit 7ea3484

Browse files
committed
derive: escape strings at compile-time when possible
1 parent f8b0481 commit 7ea3484

File tree

2 files changed

+80
-64
lines changed

2 files changed

+80
-64
lines changed

rinja_derive/src/generator.rs

+70-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use quote::quote;
1414

1515
use crate::config::WhitespaceHandling;
1616
use crate::heritage::{Context, Heritage};
17+
use crate::html::write_escaped_str;
1718
use crate::input::{Source, TemplateInput};
1819
use crate::{CompileError, MsgValidEscapers, CRATE};
1920

@@ -1162,8 +1163,76 @@ impl<'a> Generator<'a> {
11621163
}
11631164

11641165
fn write_expr(&mut self, ws: Ws, s: &'a WithSpan<'a, Expr<'a>>) {
1166+
// In here, we inspect in the expression if it is a literal, and if it is, whether it
1167+
// can be escaped at compile time. We use an IIFE to make the code more readable
1168+
// (immediate returns, try expressions).
1169+
let writable = (|| -> Option<Writable<'a>> {
1170+
enum InputKind<'a> {
1171+
StrLit(&'a str),
1172+
CharLit(&'a str),
1173+
}
1174+
enum OutputKind {
1175+
Html,
1176+
Text,
1177+
}
1178+
1179+
// for now, we only escape strings and chars at compile time
1180+
let lit = match &**s {
1181+
Expr::StrLit(input) => InputKind::StrLit(input),
1182+
Expr::CharLit(input) => InputKind::CharLit(input),
1183+
_ => return None,
1184+
};
1185+
1186+
// we only optimize for known escapers
1187+
let output = match self.input.escaper.strip_prefix(CRATE)? {
1188+
"::filters::Html" => OutputKind::Html,
1189+
"::filters::Text" => OutputKind::Text,
1190+
_ => return None,
1191+
};
1192+
1193+
// the input could be string escaped if it contains any backslashes
1194+
let escaped = match lit {
1195+
InputKind::StrLit(s) => s,
1196+
InputKind::CharLit(s) => s,
1197+
};
1198+
let unescaped = if escaped.find('\\').is_none() {
1199+
// if the literal does not contain any backslashes, then it does not need unescaping
1200+
Cow::Borrowed(escaped)
1201+
} else {
1202+
// convert the input into a TokenStream and extract the first token
1203+
Cow::Owned(match lit {
1204+
InputKind::StrLit(escaped) => {
1205+
let input = format!(r#""{escaped}""#);
1206+
let input = input.parse().ok()?;
1207+
let input = syn::parse2::<syn::LitStr>(input).ok()?;
1208+
input.value()
1209+
}
1210+
InputKind::CharLit(escaped) => {
1211+
let input = format!(r#"'{escaped}'"#);
1212+
let input = input.parse().ok()?;
1213+
let input = syn::parse2::<syn::LitChar>(input).ok()?;
1214+
input.value().to_string()
1215+
}
1216+
})
1217+
};
1218+
1219+
// escape the un-string-escaped input using the selected escaper
1220+
Some(Writable::Lit(match output {
1221+
OutputKind::Text => unescaped,
1222+
OutputKind::Html => {
1223+
let mut escaped = String::with_capacity(unescaped.len() + 20);
1224+
write_escaped_str(&mut escaped, &unescaped).ok()?;
1225+
match escaped == unescaped {
1226+
true => unescaped,
1227+
false => Cow::Owned(escaped),
1228+
}
1229+
}
1230+
}))
1231+
})()
1232+
.unwrap_or(Writable::Expr(s));
1233+
11651234
self.handle_ws(ws);
1166-
self.buf_writable.push(Writable::Expr(s));
1235+
self.buf_writable.push(writable);
11671236
}
11681237

11691238
// Write expression buffer and empty

rinja_derive/src/tests.rs

+10-63
Original file line numberDiff line numberDiff line change
@@ -440,77 +440,24 @@ fn check_escaping_at_compile_time() {
440440
r#"writer.write_str("The card is")?;
441441
match &self.suit {
442442
Suit::Clubs | Suit::Spades => {
443-
match (
444-
&((&&::rinja::filters::AutoEscaper::new(&(" black"), ::rinja::filters::Text)).rinja_auto_escape()?),
445-
) {
446-
(expr0,) => {
447-
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
448-
}
449-
}
450-
}
451-
Suit::Diamonds | Suit::Hearts => {
452-
match (
453-
&((&&::rinja::filters::AutoEscaper::new(&(" red"), ::rinja::filters::Text)).rinja_auto_escape()?),
454-
) {
455-
(expr0,) => {
456-
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
457-
}
458-
}
443+
writer.write_str(" black")?;
444+
}
445+
Suit::Diamonds | Suit::Hearts => {
446+
writer.write_str(" red")?;
459447
}
460448
}"#,
461449
&[("suit", "Suit")],
462-
14,
450+
16,
463451
);
464452

465453
compare(
466454
r#"{{ '\x41' }}{{ '\n' }}{{ '\r' }}{{ '\t' }}{{ '\\' }}{{ '\u{2665}' }}{{ '\'' }}{{ '\"' }}{{ '"' }}
467455
{{ "\x41\n\r\t\\\u{2665}\'\"'" }}"#,
468-
r#"
469-
match (
470-
&((&&::rinja::filters::AutoEscaper::new(&('\x41'), ::rinja::filters::Text))
471-
.rinja_auto_escape()?),
472-
&((&&::rinja::filters::AutoEscaper::new(&('\n'), ::rinja::filters::Text))
473-
.rinja_auto_escape()?),
474-
&((&&::rinja::filters::AutoEscaper::new(&('\r'), ::rinja::filters::Text))
475-
.rinja_auto_escape()?),
476-
&((&&::rinja::filters::AutoEscaper::new(&('\t'), ::rinja::filters::Text))
477-
.rinja_auto_escape()?),
478-
&((&&::rinja::filters::AutoEscaper::new(&('\\'), ::rinja::filters::Text))
479-
.rinja_auto_escape()?),
480-
&((&&::rinja::filters::AutoEscaper::new(
481-
&('\u{2665}'),
482-
::rinja::filters::Text,
483-
))
484-
.rinja_auto_escape()?),
485-
&((&&::rinja::filters::AutoEscaper::new(&('\''), ::rinja::filters::Text))
486-
.rinja_auto_escape()?),
487-
&((&&::rinja::filters::AutoEscaper::new(&('\"'), ::rinja::filters::Text))
488-
.rinja_auto_escape()?),
489-
&((&&::rinja::filters::AutoEscaper::new(&('"'), ::rinja::filters::Text))
490-
.rinja_auto_escape()?),
491-
&((&&::rinja::filters::AutoEscaper::new(
492-
&("\x41\n\r\t\\\u{2665}\'\"'"),
493-
::rinja::filters::Text,
494-
))
495-
.rinja_auto_escape()?),
496-
) {
497-
(expr0, expr1, expr2, expr3, expr4, expr5, expr6, expr7, expr8, expr10) => {
498-
(&&::rinja::filters::Writable(expr0)).rinja_write(writer)?;
499-
(&&::rinja::filters::Writable(expr1)).rinja_write(writer)?;
500-
(&&::rinja::filters::Writable(expr2)).rinja_write(writer)?;
501-
(&&::rinja::filters::Writable(expr3)).rinja_write(writer)?;
502-
(&&::rinja::filters::Writable(expr4)).rinja_write(writer)?;
503-
(&&::rinja::filters::Writable(expr5)).rinja_write(writer)?;
504-
(&&::rinja::filters::Writable(expr6)).rinja_write(writer)?;
505-
(&&::rinja::filters::Writable(expr7)).rinja_write(writer)?;
506-
(&&::rinja::filters::Writable(expr8)).rinja_write(writer)?;
507-
writer.write_str("
508-
")?;
509-
(&&::rinja::filters::Writable(expr10)).rinja_write(writer)?;
510-
}
511-
}
512-
"#,
456+
r#"writer.write_str("A
457+
\r \\♥'\"\"
458+
A
459+
\r \\♥'\"'")?;"#,
513460
&[],
514-
31,
461+
23,
515462
);
516463
}

0 commit comments

Comments
 (0)