Add HTML safe types

This PR adds an `HtmlSafeMarker` trait. Types that implement this marker
are know to never generate strings containing the characters `< > & " '`,
so they don't have to be escaped.

All glory goes to \@dtolnay's ["Autoref-based stable specialization"][1]
case study / blog entry.

[1]: <0a9f083f33/autoref-specialization/README.md>
This commit is contained in:
René Kijewski 2024-07-07 05:35:51 +02:00
parent 8c856c51c1
commit e3948ed436
4 changed files with 185 additions and 30 deletions

View File

@ -27,31 +27,32 @@ pub fn safe(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Displ
/// the template's extension, like `{{ val|escape("txt") }}`.
#[inline]
pub fn escape(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Display, Infallible> {
struct EscapeDisplay<T, E>(T, E);
struct EscapeWriter<W, E>(W, E);
impl<T: fmt::Display, E: Escaper> fmt::Display for EscapeDisplay<T, E> {
#[inline]
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
write!(EscapeWriter(fmt, self.1), "{}", &self.0)
}
}
impl<W: Write, E: Escaper> Write for EscapeWriter<W, E> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.1.write_escaped_str(&mut self.0, s)
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.1.write_escaped_char(&mut self.0, c)
}
}
Ok(EscapeDisplay(text, escaper))
}
pub struct EscapeDisplay<T, E>(T, E);
impl<T: fmt::Display, E: Escaper> fmt::Display for EscapeDisplay<T, E> {
#[inline]
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
struct EscapeWriter<W, E>(W, E);
impl<W: Write, E: Escaper> Write for EscapeWriter<W, E> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.1.write_escaped_str(&mut self.0, s)
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.1.write_escaped_char(&mut self.0, c)
}
}
write!(EscapeWriter(fmt, self.1), "{}", &self.0)
}
}
/// Alias for [`escape()`]
#[inline]
pub fn e(text: impl fmt::Display, escaper: impl Escaper) -> Result<impl Display, Infallible> {
@ -160,6 +161,107 @@ pub trait Escaper: Copy {
}
}
/// Used internally by rinja to select the appropriate escaper
pub trait AutoEscape {
type Escaped: fmt::Display;
type Error: Into<crate::Error>;
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error>;
}
/// Used internally by rinja to select the appropriate escaper
#[derive(Debug, Clone)]
pub struct AutoEscaper<'a, T: fmt::Display + ?Sized, E: Escaper> {
text: &'a T,
escaper: E,
}
impl<'a, T: fmt::Display + ?Sized, E: Escaper> AutoEscaper<'a, T, E> {
#[inline]
pub fn new(text: &'a T, escaper: E) -> Self {
Self { text, escaper }
}
}
/// Use the provided escaper
impl<'a, T: fmt::Display + ?Sized, E: Escaper> AutoEscape for &&AutoEscaper<'a, T, E> {
type Escaped = EscapeDisplay<&'a T, E>;
type Error = Infallible;
#[inline]
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error> {
Ok(EscapeDisplay(self.text, self.escaper))
}
}
/// Types that implement this marker trait don't need to be HTML escaped
///
/// Please note that this trait is only meant as speed-up helper. In some odd circumcises rinja
/// might still decide to HTML escape the input, so if this must not happen, then you need to use
/// the [`|safe`](super::safe) filter to prevent the auto escaping.
///
/// If you are unsure if your type generates HTML safe output in all cases, then DON'T mark it.
/// Better safe than sorry!
pub trait HtmlSafeMarker: fmt::Display {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for &T {}
/// Don't escape HTML safe types
impl<'a, T: HtmlSafeMarker + ?Sized> AutoEscape for &AutoEscaper<'a, T, Html> {
type Escaped = &'a T;
type Error = Infallible;
#[inline]
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error> {
Ok(self.text)
}
}
macro_rules! mark_html_safe {
($($ty:ty),* $(,)?) => {$(
impl HtmlSafeMarker for $ty {}
)*};
}
mark_html_safe! {
bool,
f32, f64,
i8, i16, i32, i64, i128, isize,
u8, u16, u32, u64, u128, usize,
std::num::NonZeroI8, std::num::NonZeroI16, std::num::NonZeroI32,
std::num::NonZeroI64, std::num::NonZeroI128, std::num::NonZeroIsize,
std::num::NonZeroU8, std::num::NonZeroU16, std::num::NonZeroU32,
std::num::NonZeroU64, std::num::NonZeroU128, std::num::NonZeroUsize,
}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for Box<T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::cell::Ref<'_, T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::cell::RefMut<'_, T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::rc::Rc<T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::Arc<T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::MutexGuard<'_, T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::RwLockReadGuard<'_, T> {}
impl<T: HtmlSafeMarker + ?Sized> HtmlSafeMarker for std::sync::RwLockWriteGuard<'_, T> {}
impl<T: HtmlSafeMarker> HtmlSafeMarker for std::num::Wrapping<T> {}
impl<T> HtmlSafeMarker for std::borrow::Cow<'_, T>
where
T: HtmlSafeMarker + std::borrow::ToOwned + ?Sized,
T::Owned: HtmlSafeMarker,
{
}
/// Texts are always safe
impl<'a, T: fmt::Display + ?Sized> AutoEscape for &AutoEscaper<'a, T, Text> {
type Escaped = &'a T;
type Error = Infallible;
#[inline]
fn rinja_auto_escape(&self) -> Result<Self::Escaped, Self::Error> {
Ok(self.text)
}
}
#[test]
fn test_escape() {
assert_eq!(escape("", Html).unwrap().to_string(), "");
@ -174,3 +276,53 @@ fn test_escape() {
assert_eq!(escape("<foo", Text).unwrap().to_string(), "<foo");
assert_eq!(escape("bla&h", Text).unwrap().to_string(), "bla&h");
}
#[test]
fn test_html_safe_marker() {
struct Script1;
struct Script2;
impl fmt::Display for Script1 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("<script>")
}
}
impl fmt::Display for Script2 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("<script>")
}
}
impl HtmlSafeMarker for Script2 {}
assert_eq!(
(&&AutoEscaper::new(&Script1, Html))
.rinja_auto_escape()
.unwrap()
.to_string(),
"&lt;script&gt;",
);
assert_eq!(
(&&AutoEscaper::new(&Script2, Html))
.rinja_auto_escape()
.unwrap()
.to_string(),
"<script>",
);
assert_eq!(
(&&AutoEscaper::new(&Script1, Text))
.rinja_auto_escape()
.unwrap()
.to_string(),
"<script>",
);
assert_eq!(
(&&AutoEscaper::new(&Script2, Text))
.rinja_auto_escape()
.unwrap()
.to_string(),
"<script>",
);
}

View File

@ -11,7 +11,7 @@ use std::cell::Cell;
use std::convert::Infallible;
use std::fmt::{self, Write};
pub use escape::{e, escape, safe, Escaper, Html, Text};
pub use escape::{e, escape, safe, AutoEscape, AutoEscaper, Escaper, Html, HtmlSafeMarker, Text};
#[cfg(feature = "humansize")]
use humansize::{ISizeFormatter, ToF64, DECIMAL};
#[cfg(feature = "serde_json")]

View File

@ -87,9 +87,11 @@ impl<'a> Generator<'a> {
// Implement `Template` for the given context struct.
fn impl_template(&mut self, ctx: &Context<'a>, buf: &mut Buffer) -> Result<(), CompileError> {
self.write_header(buf, format_args!("{CRATE}::Template"), None);
buf.write("fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ");
buf.write(CRATE);
buf.writeln("::Result<()> {");
buf.writeln(format_args!(
"fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) \
-> {CRATE}::Result<()> {{",
));
buf.writeln(format_args!("use {CRATE}::filters::AutoEscape as _;"));
buf.discard = self.buf_writable.discard;
// Make sure the compiler understands that the generated code depends on the template files.
@ -1166,7 +1168,7 @@ impl<'a> Generator<'a> {
let expression = match wrapped {
DisplayWrap::Wrapped => expr,
DisplayWrap::Unwrapped => format!(
"{CRATE}::filters::escape(&({expr}), {})?",
"(&&{CRATE}::filters::AutoEscaper::new(&({expr}), {})).rinja_auto_escape()?",
self.input.escaper,
),
};

View File

@ -27,6 +27,7 @@ struct Foo;"##
let expected = format!(
r#"impl ::rinja::Template for Foo {{
fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ::rinja::Result<()> {{
use ::rinja::filters::AutoEscape as _;
{new_expected}
::rinja::Result::Ok(())
}}
@ -58,7 +59,7 @@ impl ::std::fmt::Display for Foo {{
::std::write!(
writer,
"{expr0}",
expr0 = &::rinja::filters::escape(&(query), ::rinja::filters::Text)?,
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(query), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
);
@ -71,7 +72,7 @@ impl ::std::fmt::Display for Foo {{
::std::write!(
writer,
"{expr0}",
expr0 = &::rinja::filters::escape(&(s), ::rinja::filters::Text)?,
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
);
@ -84,7 +85,7 @@ impl ::std::fmt::Display for Foo {{
::std::write!(
writer,
"{expr0}",
expr0 = &::rinja::filters::escape(&(s), ::rinja::filters::Text)?,
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
);