fix(fonts): validate font set queries with Stylo

This commit is contained in:
ldm0
2026-10-02 02:23:18 +08:00
committed by Donough Liu
parent bda82c8123
commit a05bb9287c
8 changed files with 280 additions and 10 deletions
+134
View File
@@ -0,0 +1,134 @@
async function() {
const rows = [], failures = [];
const cases = [
[
"",
"SyntaxError",
"SyntaxError"
],
[
"inherit",
"SyntaxError",
"SyntaxError"
],
[
"default",
"SyntaxError",
"SyntaxError"
],
[
"12px inherit",
"SyntaxError",
"SyntaxError"
],
[
"12px default",
"SyntaxError",
"SyntaxError"
],
[
"\"inherit\"",
"SyntaxError",
"SyntaxError"
],
[
"\"default\"",
"SyntaxError",
"SyntaxError"
],
[
"12px",
"SyntaxError",
"SyntaxError"
],
[
"serif",
"SyntaxError",
"SyntaxError"
],
[
"12px serif; color: red",
"SyntaxError",
"SyntaxError"
],
[
"-1px serif",
"SyntaxError",
"SyntaxError"
],
[
"var(--x) serif",
"SyntaxError",
"SyntaxError"
],
[
"var(--x, 10px) serif",
"SyntaxError",
"SyntaxError"
],
[
"env(size) serif",
"SyntaxError",
"SyntaxError"
],
[
"12px serif !important",
"SyntaxError",
"SyntaxError"
],
[
"12px serif junk,",
"SyntaxError",
"SyntaxError"
],
[
"normal normal normal normal 12px serif",
"loaded",
true
],
[
"12px \"inherit\"",
"loaded",
true
],
[
"12px \"default\"",
"loaded",
true
],
[
"12px \"revert\"",
"loaded",
true
],
[
"italic 700 16px/1.2 \"A B\", serif",
"loaded",
true
],
[
"calc(1em + 2px) serif",
"loaded",
true
],
[
"caption",
"loaded",
true
],
[
"12px serif",
"loaded",
true
]
];
for (const [query, expectedLoad, expectedCheck] of cases) {
const load = await document.fonts.load(query).then(() => 'loaded', error => error.name);
let check;
try { check = document.fonts.check(query); } catch (error) { check = error.name; }
const row = [query, load, check];
rows.push(row);
if (load !== expectedLoad || check !== expectedCheck) failures.push({query, load, check, expectedLoad, expectedCheck});
}
return {rows, failures};
}
+3
View File
@@ -1,3 +1,6 @@
#[path = "web_apis/font_queries.rs"]
mod font_queries;
#[path = "web_apis/indexed_db_transaction.rs"]
mod indexed_db_transaction;
+26
View File
@@ -0,0 +1,26 @@
use super::pipe_disturbed::run_probe;
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn font_face_sets_validate_load_and_check_queries_with_css_shorthand_syntax() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let fixture = include_str!("../fixtures/font-query.js");
for target in ["window", "child"] {
let source =
format!("({fixture})().then(finish, error => finish({{error: String(error)}}));");
let result = run_probe(&browser, &server, target, &source).await?;
assert_eq!(
result["failures"],
serde_json::json!([]),
"{target}: {result}"
);
assert_eq!(
result["rows"].as_array().unwrap().len(),
24,
"{target}: {result}"
);
}
server.shutdown().await;
Ok(())
}
+102 -4
View File
@@ -9,8 +9,8 @@ pub fn font_load_query_contains_css_wide_keyword(query: &str) -> bool {
let mut input = Parser::new(&mut input);
while let Ok(token) = input.next() {
match token {
Token::Ident(value) | Token::QuotedString(value)
if is_css_wide_keyword(value.as_ref()) =>
Token::Ident(value)
if is_css_wide_keyword(value.as_ref()) || value.eq_ignore_ascii_case("default") =>
{
return true;
}
@@ -20,6 +20,33 @@ pub fn font_load_query_contains_css_wide_keyword(query: &str) -> bool {
false
}
/// FontFaceSet queries use the font shorthand grammar, without cascade or
/// custom-property substitution. Keep parsing shared with CSS declarations.
pub fn font_load_query_is_valid(query: &str) -> bool {
// Servo's Stylo configuration does not parse system fonts. Their entire
// shorthand syntax is one of these six identifiers; use CSS tokens so
// escapes, comments and case folding work without accepting trailing input.
let mut input = ParserInput::new(query);
let mut parser = Parser::new(&mut input);
if let Ok(name) = parser.expect_ident_cloned()
&& matches!(
name.to_ascii_lowercase().as_str(),
"caption" | "icon" | "menu" | "message-box" | "small-caption" | "status-bar"
)
&& parser.is_exhausted()
{
return true;
}
if font_load_query_contains_css_wide_keyword(query) {
return false;
}
let mut block = crate::CssDeclarationBlock::default();
let projection = block.set_property_with_projection("font", query, false);
projection.set_result != crate::CssSetResult::ParseError
&& !projection.has_unresolved_value
&& !block.is_empty()
}
pub fn font_load_query_family(query: &str) -> Option<String> {
let trimmed = query.trim();
if trimmed.is_empty() {
@@ -51,8 +78,8 @@ fn is_css_wide_keyword(value: &str) -> bool {
#[cfg(test)]
mod tests {
use super::{
font_load_query_contains_css_wide_keyword, font_load_query_family, normalize_font_face_src,
parse_font_faces,
font_load_query_contains_css_wide_keyword, font_load_query_family,
font_load_query_is_valid, normalize_font_face_src, parse_font_faces,
};
#[test]
@@ -105,6 +132,77 @@ mod tests {
);
}
#[test]
fn font_load_query_validates_the_complete_shorthand_without_substitution() {
for query in [
"",
"inherit",
"default",
"12px inherit",
"12px default",
r#""inherit""#,
"12px",
"serif",
"12px serif; color: red",
"-1px serif",
"var(--x) serif",
"var(--x, 10px) serif",
"env(size) serif",
"12px serif !important",
"12px serif,",
"caption garbage",
r#""caption""#,
] {
assert!(!font_load_query_is_valid(query), "{query}");
}
for query in [
"12px serif",
r#"12px "inherit""#,
r#"12px "default""#,
r#"italic 700 16px/1.2 "A B", serif"#,
"calc(1em + 2px) serif",
"caption",
"ICON",
"menu",
"message-box",
"small-caption",
"status-bar",
r"c\61 ption",
"caption/**/",
"normal normal normal normal 12px serif",
] {
assert!(font_load_query_is_valid(query), "{query}");
}
}
#[test]
fn font_load_query_distinguishes_reserved_identifiers_from_quoted_families() {
for keyword in [
"inherit",
"initial",
"unset",
"default",
"revert",
"revert-layer",
] {
for query in [keyword.to_owned(), format!("medium {keyword}")] {
assert!(font_load_query_contains_css_wide_keyword(&query), "{query}");
}
for query in [format!("12px \"{keyword}\""), format!("12px '{keyword}'")] {
assert!(
!font_load_query_contains_css_wide_keyword(&query),
"{query}"
);
}
}
assert!(font_load_query_contains_css_wide_keyword(
r"12px \64 efault"
));
assert!(!font_load_query_contains_css_wide_keyword(
r#"12px "\69 nherit""#
));
}
#[test]
fn font_load_query_uses_css_tokens_for_family_and_keywords() {
assert!(font_load_query_contains_css_wide_keyword(
+1 -1
View File
@@ -21,7 +21,7 @@ pub use color::{
pub use declaration::{CssDeclaration, DeclarationParseOptions, parse_declaration_list};
pub use font_face::{
CssFontFace, font_load_query_contains_css_wide_keyword, font_load_query_family,
normalize_font_face_src, parse_font_faces,
font_load_query_is_valid, normalize_font_face_src, parse_font_faces,
};
pub use font_palette::{
CssFontPaletteValuesProperty, parse_font_palette_values_property_with_stylo,
@@ -1,6 +1,6 @@
use super::events::dispatch_font_face_set_event;
use super::query::{
font_face_set_matching_faces_array, font_load_query_contains_css_wide_keyword,
font_face_set_matching_faces_array, font_load_query_is_valid,
make_rejected_dom_exception_promise,
};
use super::storage::{
@@ -28,7 +28,16 @@ pub(in crate::context_bootstrap) fn font_face_set_check_callback<'s>(
let Some(parsed) = webidl::parse_args::<FontFaceSetCheckArgs>(scope, &args) else {
return;
};
let _ = (&parsed.font, &parsed.text);
let _ = &parsed.text;
if !font_load_query_is_valid(&parsed.font) {
let error = new_dom_exception_value(
scope,
"The provided font shorthand is invalid.",
"SyntaxError",
);
scope.throw_exception(error);
return;
}
rv.set(v8::Boolean::new(scope, true).into());
}
@@ -43,7 +52,7 @@ pub(in crate::context_bootstrap) fn font_face_set_load_callback<'s>(
return;
};
let _ = &parsed.text;
if font_load_query_contains_css_wide_keyword(&parsed.font) {
if !font_load_query_is_valid(&parsed.font) {
rv.set(
make_rejected_dom_exception_promise(
scope,
@@ -2,8 +2,8 @@ use super::storage::font_face_set_faces_array;
use super::*;
use crate::util::serialize_v8_iter_array;
pub(super) fn font_load_query_contains_css_wide_keyword(query: &str) -> bool {
moli_css_parse::font_load_query_contains_css_wide_keyword(query)
pub(super) fn font_load_query_is_valid(query: &str) -> bool {
moli_css_parse::font_load_query_is_valid(query)
}
fn font_face_matches_query(