use std::borrow::Cow;
pub trait HtmlEscape {
fn escape_html(&self) -> Cow<'_, str>;
}
pub struct RawHtml(pub String);
impl RawHtml {
pub fn new<S: Into<String>>(s: S) -> Self {
RawHtml(s.into())
}
}
impl HtmlEscape for RawHtml {
fn escape_html(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.0)
}
}
#[cfg_attr(mutants, mutants::skip)]
impl HtmlEscape for String {
fn escape_html(&self) -> Cow<'_, str> {
escape_str(self)
}
}
impl HtmlEscape for &str {
fn escape_html(&self) -> Cow<'_, str> {
escape_str(self)
}
}
impl<'a> HtmlEscape for Cow<'a, str> {
fn escape_html(&self) -> Cow<'_, str> {
escape_str(self.as_ref())
}
}
#[cfg_attr(mutants, mutants::skip)]
impl<T: HtmlEscape + ?Sized> HtmlEscape for &T {
fn escape_html(&self) -> Cow<'_, str> {
(*self).escape_html()
}
}
macro_rules! impl_safe_primitives {
($($t:ty),*) => {
$(
impl HtmlEscape for $t {
fn escape_html(&self) -> Cow<'_, str> {
Cow::Owned(self.to_string())
}
}
)*
};
}
impl_safe_primitives!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool
);
#[cfg_attr(mutants, mutants::skip)]
#[inline(always)]
pub fn escape_str(s: &str) -> Cow<'_, str> {
let bytes = s.as_bytes();
let mut last_pos = 0;
let mut escaped = String::with_capacity(0);
for (i, &b) in bytes.iter().enumerate() {
let replacement = match b {
b'<' => "<",
b'>' => ">",
b'&' => "&",
b'"' => """,
b'\'' => "'",
_ => continue,
};
if last_pos == 0 {
escaped.reserve_exact(s.len() + 16);
}
escaped.push_str(&s[last_pos..i]);
escaped.push_str(replacement);
last_pos = i + 1;
}
if last_pos == 0 {
Cow::Borrowed(s)
} else {
escaped.push_str(&s[last_pos..]);
Cow::Owned(escaped)
}
}
#[inline(always)]
pub fn escape<T: HtmlEscape + ?Sized>(val: &T) -> Cow<'_, str> {
val.escape_html()
}
#[inline(always)]
pub fn escape_attr<T: HtmlEscape + ?Sized>(val: &T) -> Cow<'_, str> {
val.escape_html()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_escape_complex_javascript_types() {
let js = r#"<script>let a = {"b": 1, c: '2', d: [1,2,3]}; alert(a);</script>"#;
let expected = "<script>let a = {"b": 1, c: '2', d: [1,2,3]}; alert(a);</script>";
assert_eq!(escape_str(js), expected);
}
#[test]
fn test_escape_attr() {
let text = "<script>alert('xss')</script>";
let expected = "<script>alert('xss')</script>";
assert_eq!(escape_attr(&text), expected);
}
#[test]
fn test_escape_str_edge_cases() {
assert_eq!(escape_str(""), "");
assert_eq!(escape_str("Café & croissant"), "Café & croissant");
assert_eq!(escape_str("<script>"), "<script>");
assert_eq!(escape_str("\"'"), ""'");
}
#[test]
fn test_raw_html_new() {
let raw = RawHtml::new("<b>bold</b>");
assert_eq!(raw.0, "<b>bold</b>");
}
}
#[cfg(test)]
#[path = "html_macro_tests.rs"]
mod macro_tests;
#[cfg(kani)]
#[cfg_attr(mutants, mutants::skip)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(6)]
fn proof_escape_str_safety_and_bounds() {
let bytes: [u8; 4] = kani::any();
if let Ok(s) = std::str::from_utf8(&bytes) {
let escaped = escape_str(s);
assert!(escaped.len() >= s.len());
}
}
#[kani::proof]
#[kani::unwind(3)]
fn proof_escape_ascii_byte_exact() {
let byte: u8 = kani::any();
kani::assume(byte.is_ascii());
let input = [byte];
if let Ok(s) = std::str::from_utf8(&input) {
let escaped = escape_str(s);
let output = escaped.as_bytes();
match byte {
b'<' => {
assert!(output.len() == 4);
assert!(output[0] == b'&');
assert!(output[1] == b'l');
assert!(output[2] == b't');
assert!(output[3] == b';');
}
b'>' => {
assert!(output.len() == 4);
assert!(output[0] == b'&');
assert!(output[1] == b'g');
assert!(output[2] == b't');
assert!(output[3] == b';');
}
b'&' => {
assert!(output.len() == 5);
assert!(output[0] == b'&');
assert!(output[1] == b'a');
assert!(output[2] == b'm');
assert!(output[3] == b'p');
assert!(output[4] == b';');
}
b'"' => {
assert!(output.len() == 6);
assert!(output[0] == b'&');
assert!(output[1] == b'q');
assert!(output[2] == b'u');
assert!(output[3] == b'o');
assert!(output[4] == b't');
assert!(output[5] == b';');
}
b'\'' => {
assert!(output.len() == 6);
assert!(output[0] == b'&');
assert!(output[1] == b'#');
assert!(output[2] == b'x');
assert!(output[3] == b'2');
assert!(output[4] == b'7');
assert!(output[5] == b';');
}
_ => {
assert!(output.len() == 1);
assert!(output[0] == byte);
}
}
}
}
}