Refactor to allow more optimized escaping

This commit is contained in:
Michael Pfaff 2024-03-15 19:28:03 -04:00
parent 19559e5c09
commit 24180e1cc7
4 changed files with 85 additions and 26 deletions

View File

@ -1,21 +1,47 @@
use std::marker::PhantomData;
use array_vec::ArrayStr; use array_vec::ArrayStr;
use super::Buffer; use super::Buffer;
pub(crate) struct CommonIdents<E: Escape>(PhantomData<E>);
pub const fn are_all_chars_identity<E: Escape + ~const EscapeMeta>(set: &[char]) -> bool {
let mut i = 0;
while i < set.len() {
if !E::is_identity(set[i]) {
return false;
}
i += 1;
}
true
}
impl<E: Escape> CommonIdents<E> {
/// True if `true` and `false` will never need escaping.
pub const BOOLS: bool =
are_all_chars_identity::<E>(&['t', 'r', 'u', 'e', 'f', 'a', 'l', 's']);
/// True if unsigned integers will never need escaping.
pub const UINTS: bool =
are_all_chars_identity::<E>(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
/// True if signed integers will never need escaping.
pub const INTS: bool = Self::UINTS && are_all_chars_identity::<E>(&['-']);
/// True if floats (using [`ryu`]'s formatting) will never need escaping.
pub const FLOATS: bool = Self::INTS && are_all_chars_identity::<E>(&['.', 'e']);
}
#[const_trait]
pub trait EscapeMeta {
/// Returns `true` if the escaping scheme will never map the given character, regardless of its
/// configuration.
fn is_identity(c: char) -> bool;
}
/// A scheme for escaping strings. /// A scheme for escaping strings.
pub trait Escape { pub trait Escape: const EscapeMeta {
/// The type of an escaped character. /// The type of an escaped character.
type Escaped: AsRef<str>; type Escaped: AsRef<str>;
/// True if `true` and `false` will never need escaping.
const IDENT_BOOLS: bool = false;
/// True if unsigned integers will never need escaping.
const IDENT_UINTS: bool = false;
/// True if signed integers will never need escaping.
const IDENT_INTS: bool = false;
/// True if floats (using [`ryu`]'s formatting) will never need escaping.
const IDENT_FLOATS: bool = false;
/// If the character needs to be escaped, does so and returns it as a string. Otherwise, /// If the character needs to be escaped, does so and returns it as a string. Otherwise,
/// returns `None`. /// returns `None`.
fn escape(&self, c: char) -> Option<Self::Escaped>; fn escape(&self, c: char) -> Option<Self::Escaped>;
@ -56,14 +82,16 @@ pub trait Escape {
/// A scheme for escaping strings for safe insertion into JSON strings. /// A scheme for escaping strings for safe insertion into JSON strings.
pub struct EscapeJsonString; pub struct EscapeJsonString;
impl const EscapeMeta for EscapeJsonString {
#[inline]
fn is_identity(c: char) -> bool {
!matches!(c, '"' | '\\' | '\u{0000}'..='\u{001F}')
}
}
impl Escape for EscapeJsonString { impl Escape for EscapeJsonString {
type Escaped = ArrayStr<4>; type Escaped = ArrayStr<4>;
const IDENT_BOOLS: bool = true;
const IDENT_UINTS: bool = true;
const IDENT_INTS: bool = true;
const IDENT_FLOATS: bool = true;
#[inline] #[inline]
fn escape(&self, c: char) -> Option<Self::Escaped> { fn escape(&self, c: char) -> Option<Self::Escaped> {
match c { match c {
@ -87,3 +115,16 @@ impl Escape for EscapeJsonString {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::{CommonIdents, EscapeJsonString};
#[test]
fn check_idents() {
assert!(CommonIdents::<EscapeJsonString>::BOOLS);
assert!(CommonIdents::<EscapeJsonString>::UINTS);
assert!(CommonIdents::<EscapeJsonString>::INTS);
assert!(CommonIdents::<EscapeJsonString>::FLOATS);
}
}

View File

@ -34,20 +34,24 @@ static ESCAPE_LUT: [u8; 256] = [
const ESCAPED: [&str; 5] = ["&quot;", "&amp;", "&#039;", "&lt;", "&gt;"]; const ESCAPED: [&str; 5] = ["&quot;", "&amp;", "&#039;", "&lt;", "&gt;"];
const ESCAPED_LEN: usize = 5; const ESCAPED_LEN: usize = 5;
use crate::escape::EscapeMeta;
use super::{Buffer, Escape}; use super::{Buffer, Escape};
/// A scheme for escaping strings for safe insertion into HTML. /// A scheme for escaping strings for safe insertion into HTML.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct EscapeHtml; pub struct EscapeHtml;
impl const EscapeMeta for EscapeHtml {
#[inline]
fn is_identity(c: char) -> bool {
!matches!(c, '\"' | '&' | '<' | '>' | '\'')
}
}
impl Escape for EscapeHtml { impl Escape for EscapeHtml {
type Escaped = &'static str; type Escaped = &'static str;
const IDENT_BOOLS: bool = true;
const IDENT_UINTS: bool = true;
const IDENT_INTS: bool = true;
const IDENT_FLOATS: bool = true;
#[inline(always)] #[inline(always)]
fn escape(&self, c: char) -> Option<Self::Escaped> { fn escape(&self, c: char) -> Option<Self::Escaped> {
match c { match c {
@ -128,8 +132,18 @@ fn escape_to_buf(feed: &str, buf: &mut Buffer) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::escape;
use super::*; use super::*;
#[test]
fn check_idents() {
assert!(escape::CommonIdents::<EscapeHtml>::BOOLS);
assert!(escape::CommonIdents::<EscapeHtml>::UINTS);
assert!(escape::CommonIdents::<EscapeHtml>::INTS);
assert!(escape::CommonIdents::<EscapeHtml>::FLOATS);
}
fn escape(feed: &str) -> String { fn escape(feed: &str) -> String {
let mut s = String::new(); let mut s = String::new();
EscapeHtml.escape_to_string(&mut s, feed); EscapeHtml.escape_to_string(&mut s, feed);

View File

@ -33,6 +33,8 @@
)] )]
#![cfg_attr(sailfish_nightly, feature(core_intrinsics))] #![cfg_attr(sailfish_nightly, feature(core_intrinsics))]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![feature(const_trait_impl)]
#![feature(effects)]
#![feature(maybe_uninit_slice)] #![feature(maybe_uninit_slice)]
#![feature(specialization)] #![feature(specialization)]
#![allow(clippy::redundant_closure)] #![allow(clippy::redundant_closure)]

View File

@ -9,6 +9,8 @@ use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard}; use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard};
use crate::escape;
use super::{Buffer, Escape, SizeHint}; use super::{Buffer, Escape, SizeHint};
/// types which can be rendered inside buffer block (`<%- %>`) by reference /// types which can be rendered inside buffer block (`<%- %>`) by reference
@ -303,7 +305,7 @@ impl Render for bool {
b: &mut Buffer, b: &mut Buffer,
e: &E, e: &E,
) -> Result<(), RenderError> { ) -> Result<(), RenderError> {
if E::IDENT_BOOLS { if escape::CommonIdents::<E>::BOOLS {
self.render(b) self.render(b)
} else { } else {
if *self { if *self {
@ -341,7 +343,7 @@ macro_rules! render_int {
#[inline] #[inline]
fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> { fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
if E::$ident_tag { if escape::CommonIdents::<E>::$ident_tag {
// push_str without escape // push_str without escape
self.render(b) self.render(b)
} else { } else {
@ -353,8 +355,8 @@ macro_rules! render_int {
} }
} }
render_int!(IDENT_UINTS, u8, u16, u32, u64, u128, usize); render_int!(UINTS, u8, u16, u32, u64, u128, usize);
render_int!(IDENT_INTS, i8, i16, i32, i64, i128, isize); render_int!(INTS, i8, i16, i32, i64, i128, isize);
impl Render for f32 { impl Render for f32 {
#[cfg_attr(feature = "perf-inline", inline)] #[cfg_attr(feature = "perf-inline", inline)]
@ -384,7 +386,7 @@ impl Render for f32 {
b: &mut Buffer, b: &mut Buffer,
e: &E, e: &E,
) -> Result<(), RenderError> { ) -> Result<(), RenderError> {
if E::IDENT_FLOATS { if escape::CommonIdents::<E>::FLOATS {
self.render(b) self.render(b)
} else { } else {
default_render_escaped(self, b, e) default_render_escaped(self, b, e)
@ -420,7 +422,7 @@ impl Render for f64 {
b: &mut Buffer, b: &mut Buffer,
e: &E, e: &E,
) -> Result<(), RenderError> { ) -> Result<(), RenderError> {
if E::IDENT_FLOATS { if escape::CommonIdents::<E>::FLOATS {
self.render(b) self.render(b)
} else { } else {
default_render_escaped(self, b, e) default_render_escaped(self, b, e)