Merge pull request #5408 from storytold/fix/sec-svg-css-string-escapes

CSS export: line breaks and angle brackets in names can't end a CSS string
This commit is contained in:
BFlat
2026-11-03 23:53:08 +07:00
committed by GitHub
+31 -3
View File
@@ -26,7 +26,28 @@ pub(crate) fn declarations<'a>(props: impl IntoIterator<Item = &'a (&'static str
/// A CSS string.
pub(crate) fn css_string(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
quoted(s, '"')
}
/// `s` as a CSS string in `quote`s. A backslash and the quote are escaped, and so are line breaks
/// and other control characters (a newline would end the string and let the rest be rules) and
/// `<` and `>` (which could end a `<style>` element), as hex escapes.
fn quoted(s: &str, quote: char) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
c if c == quote => {
out.push('\\');
out.push(c);
}
c if c.is_control() || matches!(c, '<' | '>') => out.push_str(&format!("\\{:x} ", u32::from(c))),
c => out.push(c),
}
}
out.push(quote);
out
}
pub(crate) fn blend_css(b: BlendMode) -> &'static str {
@@ -108,11 +129,11 @@ fn generic_fallback(family: &str) -> &'static str {
/// and the `wdth` setting is `font-stretch` too ([`font_stretch`]).
pub(crate) fn font_props(st: &CharStyle, len: &dyn Fn(f64) -> String) -> Props {
let mut p = Props::new();
// A quoted family escapes `\` and `'`, which would otherwise end the string early.
// A quoted family escapes `\`, `'` and line breaks, which would otherwise end the string early.
// Only a plain identifier (no leading digit) can be written bare; anything else is quoted.
let bare = st.font_family.chars().next().is_some_and(|c| !c.is_ascii_digit() && c != '-')
&& st.font_family.chars().all(|c| c.is_alphanumeric() || matches!(c, '-' | '_'));
let fam = if !bare { format!("'{}'", st.font_family.replace('\\', "\\\\").replace('\'', "\\'")) } else { st.font_family.clone() };
let fam = if !bare { quoted(&st.font_family, '\'') } else { st.font_family.clone() };
p.push(("font-family", format!("{fam}{}", generic_fallback(&st.font_family))));
p.push(("font-size", len(st.size * st.v_scale / 100.0)));
let (weight, italic) = font_descriptor(st);
@@ -751,4 +772,11 @@ mod tests {
}
assert_eq!(w.next_suffix["b"], 20_001);
}
#[test]
fn line_breaks_and_tags_cannot_end_a_string() {
assert!(family("a\n} * {color:red").starts_with(r"'a\a } * {color:red'"), "{}", family("a\n} * {color:red"));
assert_eq!(css_string("w\r\"</style>"), r#""w\d \"\3c /style\3e ""#);
assert_eq!(css_string("wght"), "\"wght\"");
}
}