diff --git a/Cargo.lock b/Cargo.lock index b864df8b..740bbc38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,7 @@ name = "anstyle-svg" version = "1.1.0" dependencies = [ "anstyle 1.0.14", + "anstyle-hyperlink", "anstyle-lossy", "anstyle-parse 1.0.0", "html-escape", diff --git a/crates/anstyle-svg/Cargo.toml b/crates/anstyle-svg/Cargo.toml index a15fb76a..881233d5 100644 --- a/crates/anstyle-svg/Cargo.toml +++ b/crates/anstyle-svg/Cargo.toml @@ -31,6 +31,7 @@ html-escape = "0.2.13" unicode-width = "0.2.2" [dev-dependencies] +anstyle-hyperlink = { path = "../anstyle-hyperlink" } proptest = "1.7.0" snapbox = "0.6.23" diff --git a/crates/anstyle-svg/src/lib.rs b/crates/anstyle-svg/src/lib.rs index e3756847..975f470e 100644 --- a/crates/anstyle-svg/src/lib.rs +++ b/crates/anstyle-svg/src/lib.rs @@ -489,6 +489,7 @@ fn write_fg_span(buffer: &mut String, span: &str, element: &adapter::Element, fr } write!(buffer, r#">"#).unwrap(); if let Some(hyperlink) = &element.url { + let hyperlink = sanitize_hyperlink(hyperlink); write!(buffer, r#""#).unwrap(); need_closing_a = true; } @@ -643,6 +644,26 @@ fn split_lines(styled: &[adapter::Element]) -> Vec> { lines } +fn sanitize_hyperlink(link: &str) -> String { + html_escape::encode_double_quoted_attribute(link).into_owned() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn link_injection() { + let link = anstyle_hyperlink::Hyperlink::with_url("https://example.com/\">"); + let input = format!("Hello {link}world{link:#}!"); + let mut styled = adapter::AnsiBytes::new(); + let elements = styled.extract_next(input.as_bytes()).collect::>(); + let actual = elements.into_iter().find_map(|e| e.url).unwrap(); + let actual = sanitize_hyperlink(&actual); + snapbox::assert_data_eq!(actual, snapbox::str!["https://example.com/">"]); + } +} + #[doc = include_str!("../README.md")] #[cfg(doctest)] pub struct ReadmeDoctests;