use alloc::borrow::Cow;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use crate::error::Result;
use crate::value::{Value, ValueFunc};
use super::primitives::{
ContentType, FILTER_FAILSAFE, HtmlTable, JsTable, css_escaper, css_value_filter,
html_name_filter, html_replacer, is_safe_url, js_replace, js_val_escaper, srcset_filter,
stringify, strip_tags, url_processor,
};
type EscaperFn = fn(&[Value]) -> Result<Value>;
pub(crate) const ESCAPERS: [(&str, EscaperFn); 17] = [
("_html_template_attrescaper", attr_escaper),
("_html_template_commentescaper", comment_escaper),
("_html_template_cssescaper", css_escaper_fn),
("_html_template_cssvaluefilter", css_value_filter_fn),
("_html_template_htmlnamefilter", html_name_filter_escaper),
("_html_template_htmlescaper", html_escaper),
("_html_template_jsregexpescaper", js_regexp_escaper),
("_html_template_jsstrescaper", js_str_escaper),
("_html_template_jstmpllitescaper", js_tmpl_lit_escaper),
("_html_template_jsvalescaper", js_val_escaper_fn),
("_html_template_nospaceescaper", nospace_escaper),
("_html_template_rcdataescaper", rcdata_escaper),
("_html_template_srcsetescaper", srcset_escaper),
("_html_template_urlescaper", url_escaper),
("_html_template_urlfilter", url_filter),
("_html_template_urlnormalizer", url_normalizer),
("_eval_args_", eval_args),
];
fn str_value(s: &str) -> Value {
Value::String(Arc::from(s))
}
#[inline]
fn reuse_input(args: &[Value]) -> Option<Value> {
if let [single] = args {
match single {
Value::String(s) => return Some(Value::String(Arc::clone(s))),
Value::Safe { s, .. } => return Some(Value::String(Arc::clone(s))),
_ => {}
}
}
None
}
#[inline]
fn finish(args: &[Value], result: Cow<'_, str>) -> Value {
match result {
Cow::Borrowed(b) => reuse_input(args).unwrap_or_else(|| str_value(b)),
Cow::Owned(s) => str_value(&s),
}
}
fn html_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
if t == ContentType::Html {
return Ok(reuse_input(args).unwrap_or_else(|| str_value(s.as_ref())));
}
Ok(finish(args, html_replacer(&s, HtmlTable::Html, true)))
}
fn attr_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
if t == ContentType::Html {
let stripped = strip_tags(&s);
let normalized = html_replacer(&stripped, HtmlTable::HtmlNorm, true);
if matches!(stripped, Cow::Borrowed(_)) {
return Ok(finish(args, normalized));
}
return Ok(str_value(&normalized));
}
Ok(finish(args, html_replacer(&s, HtmlTable::Html, true)))
}
fn rcdata_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
let table = if t == ContentType::Html {
HtmlTable::HtmlNorm
} else {
HtmlTable::Html
};
Ok(finish(args, html_replacer(&s, table, true)))
}
fn nospace_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
if s.is_empty() {
return Ok(str_value(FILTER_FAILSAFE));
}
if t == ContentType::Html {
let stripped = strip_tags(&s);
let normalized = html_replacer(&stripped, HtmlTable::NospaceNorm, false);
if matches!(stripped, Cow::Borrowed(_)) {
return Ok(finish(args, normalized));
}
return Ok(str_value(&normalized));
}
Ok(finish(args, html_replacer(&s, HtmlTable::Nospace, false)))
}
fn comment_escaper(_args: &[Value]) -> Result<Value> {
Ok(str_value(""))
}
fn html_name_filter_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
Ok(str_value(html_name_filter(&s, t).as_ref()))
}
fn css_escaper_fn(args: &[Value]) -> Result<Value> {
let (s, _) = stringify(args);
Ok(finish(args, css_escaper(&s)))
}
fn css_value_filter_fn(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
Ok(str_value(css_value_filter(&s, t).as_ref()))
}
fn js_val_escaper_fn(args: &[Value]) -> Result<Value> {
Ok(str_value(&js_val_escaper(args)))
}
fn js_str_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
let table = if t == ContentType::JsStr {
JsTable::StrNorm
} else {
JsTable::Str
};
Ok(finish(args, js_replace(&s, table)))
}
fn js_tmpl_lit_escaper(args: &[Value]) -> Result<Value> {
let (s, _) = stringify(args);
Ok(finish(args, js_replace(&s, JsTable::BqStr)))
}
fn js_regexp_escaper(args: &[Value]) -> Result<Value> {
let (s, _) = stringify(args);
let out = js_replace(&s, JsTable::Regexp);
if out.is_empty() {
return Ok(str_value("(?:)"));
}
Ok(finish(args, out))
}
fn url_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
Ok(finish(args, url_processor(false, &s, t)))
}
fn url_normalizer(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
Ok(finish(args, url_processor(true, &s, t)))
}
fn url_filter(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
if t != ContentType::Url && !is_safe_url(&s) {
return Ok(str_value("#ZgotmplZ"));
}
Ok(reuse_input(args).unwrap_or_else(|| str_value(s.as_ref())))
}
fn srcset_escaper(args: &[Value]) -> Result<Value> {
let (s, t) = stringify(args);
Ok(finish(args, srcset_filter(&s, t)))
}
fn eval_args(args: &[Value]) -> Result<Value> {
if args.len() == 1
&& let Value::String(s) = &args[0]
{
return Ok(Value::String(Arc::clone(s)));
}
Ok(str_value(&crate::go::sprint(args)))
}
pub(crate) fn merge(base: &Arc<BTreeMap<String, ValueFunc>>) -> Arc<BTreeMap<String, ValueFunc>> {
let mut funcs = base.clone();
register(Arc::make_mut(&mut funcs));
funcs
}
fn register(m: &mut BTreeMap<String, ValueFunc>) {
for (name, f) in ESCAPERS {
let vf: ValueFunc = Arc::new(f);
m.insert(name.to_string(), vf);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn call(name: &str, args: &[Value]) -> String {
let base: Arc<BTreeMap<String, ValueFunc>> = Arc::new(BTreeMap::new());
let funcs = merge(&base);
let f = funcs.get(name).expect("escaper registered");
match f(args).expect("escaper ok") {
Value::String(s) => s.to_string(),
other => panic!("expected string, got {other:?}"),
}
}
fn s(v: &str) -> Value {
Value::String(Arc::from(v))
}
#[test]
fn all_names_registered() {
let base: Arc<BTreeMap<String, ValueFunc>> = Arc::new(BTreeMap::new());
let funcs = merge(&base);
for (name, _) in ESCAPERS {
assert!(funcs.contains_key(name), "missing {name}");
}
}
#[test]
fn html_escaper_text_and_safe() {
assert_eq!(
call(
"_html_template_htmlescaper",
&[s("<b> \"foo%\" O'Reilly &bar;")]
),
"<b> "foo%" O'Reilly &bar;"
);
assert_eq!(
call(
"_html_template_htmlescaper",
&[Value::Safe {
kind: crate::SafeKind::Html,
s: Arc::from("Hello, <b>World</b> &tc!"),
}]
),
"Hello, <b>World</b> &tc!"
);
}
#[test]
fn attr_escaper_strips_tags_for_html() {
assert_eq!(
call(
"_html_template_attrescaper",
&[Value::Safe {
kind: crate::SafeKind::Html,
s: Arc::from("Hello, <b>World</b> &tc!"),
}]
),
"Hello, World &tc!"
);
}
#[test]
fn nospace_escaper_empty_is_failsafe() {
assert_eq!(call("_html_template_nospaceescaper", &[s("")]), "ZgotmplZ");
assert_eq!(
call(
"_html_template_nospaceescaper",
&[s("<b> \"foo%\" O'Reilly &bar;")]
),
"<b> "foo%" O'Reilly &bar;"
);
}
#[test]
fn comment_escaper_drops_everything() {
assert_eq!(call("_html_template_commentescaper", &[s("anything")]), "");
}
#[test]
fn js_val_escaper_via_funcmap() {
assert_eq!(
call("_html_template_jsvalescaper", &[Value::Int(42)]),
" 42 "
);
assert_eq!(call("_html_template_jsvalescaper", &[s("foo")]), "\"foo\"");
}
#[test]
fn url_filter_defangs_javascript() {
assert_eq!(
call("_html_template_urlfilter", &[s("javascript:alert(1)")]),
"#ZgotmplZ"
);
assert_eq!(
call("_html_template_urlfilter", &[s("http://example.com")]),
"http://example.com"
);
assert_eq!(
call(
"_html_template_urlfilter",
&[Value::Safe {
kind: crate::SafeKind::Url,
s: Arc::from("javascript:ok()"),
}]
),
"javascript:ok()"
);
}
#[test]
fn url_escaper_and_normalizer() {
assert_eq!(
call(
"_html_template_urlescaper",
&[s("greeting=H%69,&addressee=(World)")]
),
"greeting%3dH%2569%2c%26addressee%3d%28World%29"
);
assert_eq!(
call(
"_html_template_urlnormalizer",
&[s("greeting=H%69,&addressee=(World)")]
),
"greeting=H%69,&addressee=%28World%29"
);
}
#[test]
fn eval_args_sprint() {
assert_eq!(call("_eval_args_", &[s("hi")]), "hi");
assert_eq!(call("_eval_args_", &[Value::Int(1), Value::Int(2)]), "1 2");
assert_eq!(call("_eval_args_", &[Value::Nil]), "<nil>");
}
}