rullst-core 12.3.0

Core runtime, routing engine, and telemetry for the Rullst framework suite.
Documentation
//! Runtime behaviour of code generated by `#[island]` and `html!`.

#![allow(clippy::unwrap_used, clippy::expect_used)]

/// A value whose `Serialize` implementation always fails.
struct Unserializable;

impl serde::Serialize for Unserializable {
    fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
        Err(serde::ser::Error::custom("deliberately unserializable"))
    }
}

#[crate::island]
fn wide_counter(start: u128) -> String {
    format!("<b>{start}</b>")
}

#[crate::island]
fn opaque_widget(value: Unserializable) -> String {
    let _ = value;
    "<i>static</i>".to_string()
}

#[test]
fn island_renders_without_hydration_props_instead_of_panicking() {
    assert_eq!(
        wide_counter(5),
        "<div data-island=\"wide_counter\" data-props=\"{&quot;start&quot;:5}\"><b>5</b></div>"
    );
    let wide = u128::from(u64::MAX) + 1;
    assert_eq!(
        wide_counter(wide),
        format!("<div data-island=\"wide_counter\" data-props=\"\"><b>{wide}</b></div>")
    );
    assert_eq!(
        opaque_widget(Unserializable),
        "<div data-island=\"opaque_widget\" data-props=\"\"><i>static</i></div>"
    );
}

const SCRIPT_URLS: [&str; 8] = [
    "javascript:alert(1)",
    " JaVaScRiPt:alert(1)",
    "java\tscript:alert(1)",
    "java\nscript\r:alert(1)",
    "\u{1}\u{20}javascript:alert(1)",
    "vbscript:msgbox(1)",
    "VBScript:msgbox(1)",
    "data:text/html,<script>alert(1)</script>",
];

#[test]
fn dynamic_url_attributes_neutralize_script_schemes() {
    for url in SCRIPT_URLS {
        assert_eq!(
            crate::html! { <a href={url}>"x"</a> },
            "<a href=\"#\">x</a>",
            "{url:?}"
        );
        assert_eq!(
            crate::html! {
                <form action={url}><button formaction={url}>"go"</button></form>
            },
            "<form action=\"#\"><button formaction=\"#\">go</button></form>"
        );
        assert_eq!(
            crate::html! { <iframe src={url}></iframe> },
            "<iframe src=\"#\"></iframe>"
        );
        // Attribute names are matched case-insensitively.
        assert_eq!(
            crate::html! { <a HREF={url}>"x"</a> },
            "<a HREF=\"#\">x</a>"
        );
        // An explicit RawHtml value is still checked.
        assert_eq!(
            crate::html! { <a href={crate::html::RawHtml::new(url)}>"x"</a> },
            "<a href=\"#\">x</a>"
        );
    }
}

#[test]
fn safe_urls_and_other_attributes_keep_plain_escaping() {
    for (url, rendered) in [
        (
            "https://example.com/?a=1&b=\"2\"",
            "https://example.com/?a=1&amp;b=&quot;2&quot;",
        ),
        ("/profile/7", "/profile/7"),
        ("#section", "#section"),
        ("mailto:team@example.com", "mailto:team@example.com"),
        ("./a:b", "./a:b"),
        ("javascript", "javascript"),
        // Entities in text are escaped, so the browser never decodes a colon.
        ("javascript&colon;alert(1)", "javascript&amp;colon;alert(1)"),
    ] {
        assert_eq!(
            crate::html! { <a href={url}>"x"</a> },
            format!("<a href=\"{rendered}\">x</a>"),
            "{url:?}"
        );
    }
    // Temporaries such as `format!(..)` live until the value is written.
    assert_eq!(
        crate::html! { <a href={format!("/posts/{}", 7)}>"x"</a> },
        "<a href=\"/posts/7\">x</a>"
    );
    // Only URL attributes are rewritten.
    assert_eq!(
        crate::html! { <p title={"javascript:alert(1)"}>"x"</p> },
        "<p title=\"javascript:alert(1)\">x</p>"
    );
    // Static attributes are author-owned source and stay verbatim.
    assert_eq!(
        crate::html! { <a href="javascript:void(0)">"x"</a> },
        "<a href=\"javascript:void(0)\">x</a>"
    );
}

#[test]
fn data_urls_are_kept_only_on_media_elements() {
    let image = "data:image/png;base64,iVBORw0KGgo=";
    assert_eq!(
        crate::html! { <img src={image} /> },
        format!("<img src=\"{image}\" />")
    );
    assert_eq!(
        crate::html! { <video poster={image}><source src={image} /></video> },
        format!("<video poster=\"{image}\"><source src=\"{image}\" /></video>")
    );
    assert_eq!(
        crate::html! { <img src={"javascript:alert(1)"} /> },
        "<img src=\"#\" />"
    );
    assert_eq!(
        crate::html! { <a href={image}>"x"</a> },
        "<a href=\"#\">x</a>"
    );
    assert_eq!(
        crate::html! { <embed src={image} /> },
        "<embed src=\"#\" />"
    );
}