From cb3762b9066f1b266d4aa72854cc729e2ddccdda Mon Sep 17 00:00:00 2001 From: Jonathan Cornaz Date: Thu, 31 Oct 2024 21:36:49 +0100 Subject: [PATCH] add `raw` and `raw_unsafe` escape hatches --- CHANGELOG.md | 3 ++- src/attributes.rs | 8 ++------ src/lib.rs | 2 ++ src/nodes.rs | 20 ++++++++++++++++++++ tests/render_spec.rs | 12 +++++++----- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b661f..e22bd59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Escape HTML strings * `id` and `class` attributes * Nodes - * Html document (`html`, `head`, `body`) + * Html document (`html`, `head` and `body`) * Meta (`title`) * Text (`h1`, `h2`, `h3`, `h4`, `h5`, `h6` and `text`) * Container (`div`) + * Escape hatches (`raw and `raw_unsafe`) * `Node`, `Attribute` and `Document` types [Unreleased]: https://github.com/jcornaz/fun-html/compare/...HEAD diff --git a/src/attributes.rs b/src/attributes.rs index f7898ea..57c227c 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -8,16 +8,12 @@ impl>> From<(&'static str, T)> for Attribute { } } -pub fn attr(key: &'static str, value: impl Into>) -> Attribute { - Attribute::new(key, value) -} - pub fn lang(lang: impl Into>) -> Attribute { - attr("lang", lang) + ("lang", lang).into() } pub fn id(id: impl Into>) -> Attribute { - attr("id", id) + ("id", id).into() } pub fn class<'a>(classes: impl IntoIterator) -> Attribute { diff --git a/src/lib.rs b/src/lib.rs index a06eb3f..f679fac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ enum NodeInner { children: Vec, }, Text(Cow<'static, str>), + Raw(Cow<'static, str>), } #[derive(Debug, Clone)] @@ -100,6 +101,7 @@ impl Display for Node { write!(f, "")?; } NodeInner::Text(text) => write!(f, "{}", html_escape::encode_text(text))?, + NodeInner::Raw(raw) => write!(f, "{raw}")?, } Ok(()) } diff --git a/src/nodes.rs b/src/nodes.rs index 18d165b..bd39d0f 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -72,10 +72,30 @@ pub fn h6( Node::new("h6", attributes, children) } +/// HTML escaped text pub fn text(value: impl Into>) -> Node { NodeInner::Text(value.into()).into() } +/// Inline raw HTML without escaping +/// +/// This function is considered safe because the HTML being inlined must be known at compile time +/// +/// See [`raw_unsafe`] to inline HTML that is generated at runtime +pub fn raw(html: &'static str) -> Node { + NodeInner::Raw(html.into()).into() +} + +/// Inline raw HTML without escaping +/// +/// This function **IS NOT SAFE** and should be avoided unless really necessary. +/// Miss-use can lead to XSS vulnerability. +/// +/// See [`raw`] to safely inline HTML that is known at compile time +pub fn raw_unsafe(html: String) -> Node { + NodeInner::Raw(html.into()).into() +} + impl>> From for Node { fn from(value: T) -> Self { text(value) diff --git a/tests/render_spec.rs b/tests/render_spec.rs index e7771f2..911fb39 100644 --- a/tests/render_spec.rs +++ b/tests/render_spec.rs @@ -27,7 +27,7 @@ fn should_render_html_document() { #[test] fn should_render_attributes() { let node = h1( - [class(["underlined", "blue"]), attr("foo", "bar")], + [class(["underlined", "blue"]), ("foo", "bar").into()], [text("Hello world!")], ); assert_eq!( @@ -41,10 +41,12 @@ fn should_render_attributes() { #[case(text("hello".to_string()), "hello")] #[case("hello".into(), "hello")] #[case("hello".to_string().into(), "hello")] -#[case(div([attr("foo", "bar")], ["hello".into()]), "
hello
")] -#[case(div([attr("foo", "bar".to_string())], [text("hello".to_string())]), "
hello
")] +#[case(raw(""), "")] +#[case(raw_unsafe("".to_string()), "")] +#[case(div([("foo", "bar").into()], ["hello".into()]), "
hello
")] +#[case(div([("foo", "bar".to_string()).into()], [text("hello".to_string())]), "
hello
")] #[case(head([id("foo")], [text("hello")]), "hello")] -#[case(title([attr("foo", "bar")], [text("hello")]), "hello")] +#[case(title([("foo", "bar").into()], [text("hello")]), "hello")] #[case(body([id("foo")], [text("hello")]), "hello")] #[case(h1([id("foo")], [text("hello")]), "

hello

")] #[case(h2([id("foo")], [text("hello")]), "

hello

")] @@ -66,7 +68,7 @@ fn text_should_be_escaped() { #[rstest] fn attribute_should_be_escaped() { let string = div( - [attr("foo", "