use std::fmt;
use std::ops::Deref;
pub struct RawHtml {
html: Box<str>,
}
impl RawHtml {
pub fn trusted(html: impl Into<String>) -> RawHtml {
let html = html.into().into_boxed_str();
provenance::remember(&html);
RawHtml { html }
}
pub fn as_str(&self) -> &str {
&self.html
}
}
impl Deref for RawHtml {
type Target = str;
fn deref(&self) -> &str {
&self.html
}
}
impl Clone for RawHtml {
fn clone(&self) -> Self {
RawHtml::trusted(&*self.html)
}
}
impl Drop for RawHtml {
fn drop(&mut self) {
provenance::forget(&self.html);
}
}
impl PartialEq for RawHtml {
fn eq(&self, other: &Self) -> bool {
self.html == other.html
}
}
impl Eq for RawHtml {}
impl fmt::Debug for RawHtml {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("RawHtml").field(&&*self.html).finish()
}
}
pub(crate) fn check_body_prefix(body_prefix: &str, route: &str) {
#[cfg(debug_assertions)]
{
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if WARNED.load(Ordering::Relaxed)
|| body_prefix.is_empty()
|| provenance::is_raw(body_prefix)
{
return;
}
if !WARNED.swap(true, Ordering::Relaxed) {
tracing::warn!(
code = "RAW-101",
route,
"RAW-101 PageConfig::body_prefix was given a plain string on route {route}: \
bind the markup as a RawHtml first (let markup = RawHtml::trusted(..);) and \
pass body_prefix: Some(&markup), so review and CI can find it; 3.0.0 accepts \
only a RawHtml. Logged once per process, in debug builds."
);
}
}
#[cfg(not(debug_assertions))]
let _ = (body_prefix, route);
}
#[cfg(debug_assertions)]
mod provenance {
use std::collections::BTreeSet;
use std::sync::{Mutex, MutexGuard};
static LIVE: Mutex<BTreeSet<(usize, usize)>> = Mutex::new(BTreeSet::new());
fn key(s: &str) -> (usize, usize) {
(s.as_ptr() as usize, s.len())
}
fn live() -> MutexGuard<'static, BTreeSet<(usize, usize)>> {
LIVE.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(super) fn remember(s: &str) {
if !s.is_empty() {
live().insert(key(s));
}
}
pub(super) fn forget(s: &str) {
if !s.is_empty() {
live().remove(&key(s));
}
}
pub(super) fn is_raw(s: &str) -> bool {
!s.is_empty() && live().contains(&key(s))
}
#[cfg(test)]
mod tests {
use super::is_raw;
use crate::RawHtml;
#[test]
fn a_raw_html_is_known_by_its_markup_and_a_string_with_the_same_text_is_not() {
let raw = RawHtml::trusted("<nav>n</nav>");
let same_text = String::from("<nav>n</nav>");
assert!(is_raw(&raw));
assert!(is_raw(raw.as_str()));
assert!(!is_raw(&same_text));
assert!(!is_raw(&raw[1..]));
assert!(!is_raw(&raw[..raw.len() - 1]));
let copy = raw.clone();
assert_eq!(copy, raw);
assert!(is_raw(©));
let at = raw.as_ptr();
let moved = Box::new(raw);
assert_eq!(moved.as_ptr(), at);
assert!(is_raw(&moved));
}
#[test]
fn a_dropped_raw_html_is_forgotten() {
let raw = RawHtml::trusted("<aside>".repeat(41));
let (at, len) = (raw.as_ptr() as usize, raw.len());
assert!(super::live().contains(&(at, len)));
drop(raw);
assert!(!super::live().contains(&(at, len)));
}
#[test]
fn empty_markup_is_never_raw() {
let raw = RawHtml::trusted("");
assert!(!is_raw(&raw));
assert!(!is_raw(""));
}
}
}
#[cfg(not(debug_assertions))]
mod provenance {
pub(super) fn remember(_: &str) {}
pub(super) fn forget(_: &str) {}
}