mirror of
https://github.com/lexmount/moli.git
synced 2026-10-01 08:00:38 +00:00
fix(cssom): validate font feature rule and map receivers
Enable the shared native-brand receiver checks on CSSFontFeatureValuesRule attributes and CSSFontFeatureValuesMap operations. Invalid receivers now throw the callee realm TypeError before argument conversion. Regression coverage includes genuine cross-realm objects, forged and author/revoked Proxy receivers, conversion ordering, retained rules/maps after removal, and native objects with changed JavaScript prototypes. Validation: fmt, strict workspace clippy, full nextest. Summary [ 107.439s] 18906 tests run: 18906 passed (9 slow), 16 skipped
This commit is contained in:
@@ -10,7 +10,7 @@ struct CssFontFeatureValuesMapObjectDeclaration<'s> {
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::CSSFontFeatureValuesMap, enumerable)]
|
||||
#[webapi(interface = web_api_interfaces::CSSFontFeatureValuesMap, enumerable, receiver)]
|
||||
struct CssFontFeatureValuesMapPrototypeDeclaration {
|
||||
#[webapi(accessor_property, getter = css_font_feature_values_map_size_getter_callback)]
|
||||
size: (),
|
||||
|
||||
@@ -654,7 +654,7 @@ pub struct CssMarginRulePrototypeDeclaration {
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::CSSFontFeatureValuesRule, enumerable)]
|
||||
#[webapi(interface = web_api_interfaces::CSSFontFeatureValuesRule, enumerable, receiver)]
|
||||
pub struct CssFontFeatureValuesRulePrototypeDeclaration {
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
const equal = (actual, expected, label) => {
|
||||
if (!Object.is(actual, expected)) throw new Error(`${label}: ${String(actual)} !== ${String(expected)}`);
|
||||
};
|
||||
const realms = [globalThis, document.getElementById('child').contentWindow];
|
||||
const records = realms.map(realm => {
|
||||
const sheet = new realm.CSSStyleSheet();
|
||||
sheet.replaceSync('@font-feature-values Seed { @styleset { existing: 2 3; } }');
|
||||
const rule = sheet.cssRules[0];
|
||||
return { realm, sheet, rule, map: rule.styleset };
|
||||
});
|
||||
const prototypes = records.map(record => Object.getPrototypeOf(record.map));
|
||||
const prototypeFor = realm => prototypes[realms.indexOf(realm)];
|
||||
let conversions = 0, traps = 0, callbacks = 0;
|
||||
const key = { toString() { conversions++; return 'late'; } };
|
||||
const values = { get [Symbol.iterator]() { conversions++; return [7][Symbol.iterator]; } };
|
||||
const handler = {
|
||||
get() { traps++; throw new Error('author get trap'); },
|
||||
getPrototypeOf() { traps++; throw new Error('author prototype trap'); }
|
||||
};
|
||||
const revoked = Proxy.revocable(records[0].map, handler); revoked.revoke();
|
||||
const invalid = [null, undefined, false, 1, 'text', Symbol(), 1n, {}, new Map(), revoked.proxy];
|
||||
for (const { realm, rule, map } of records) {
|
||||
invalid.push(rule, prototypeFor(realm), Object.create(map),
|
||||
Object.create(prototypeFor(realm)), new Proxy(map, handler));
|
||||
}
|
||||
for (const realm of realms) {
|
||||
const p = prototypeFor(realm);
|
||||
const size = Object.getOwnPropertyDescriptor(p, 'size').get;
|
||||
const operations = [
|
||||
['size', size, []], ['get', p.get, [key]], ['has', p.has, [key]],
|
||||
['set', p.set, [key, values]], ['delete', p.delete, [key]], ['clear', p.clear, []],
|
||||
['entries', p.entries, []], ['keys', p.keys, []], ['values', p.values, []],
|
||||
['iterator', p[Symbol.iterator], []], ['forEach', p.forEach, [() => { callbacks++; }]]
|
||||
];
|
||||
equal(p[Symbol.iterator], p.entries, 'iterator alias identity');
|
||||
for (const [name, fn, args] of operations) {
|
||||
for (const receiver of invalid) {
|
||||
let thrown = false;
|
||||
try { Reflect.apply(fn, receiver, args); } catch (error) {
|
||||
equal(Object.getPrototypeOf(error), realm.TypeError.prototype, `${name} callee TypeError realm`);
|
||||
thrown = true;
|
||||
}
|
||||
equal(thrown, true, `${name} rejects incompatible receiver`);
|
||||
}
|
||||
}
|
||||
}
|
||||
equal(conversions, 0, 'receiver check precedes conversion');
|
||||
equal(traps, 0, 'no Proxy traps');
|
||||
equal(callbacks, 0, 'no callback on invalid receiver');
|
||||
for (const { sheet, rule, map } of records) {
|
||||
for (const detached of [false, true]) {
|
||||
if (detached) sheet.deleteRule(0);
|
||||
for (const realm of realms) {
|
||||
const p = prototypeFor(realm);
|
||||
p.clear.call(map);
|
||||
equal(p.set.call(map, 'late', [7, 8]), undefined, 'native map set return');
|
||||
equal(p.has.call(map, 'late'), true, 'borrowed native has');
|
||||
equal(JSON.stringify(p.get.call(map, 'late')), '[7,8]', 'borrowed native get');
|
||||
equal(Object.getOwnPropertyDescriptor(p, 'size').get.call(map), 1, 'native size');
|
||||
equal(JSON.stringify(Array.from(p.entries.call(map))), '[["late",[7,8]]]', 'native entries');
|
||||
equal(JSON.stringify(Array.from(p.keys.call(map))), '["late"]', 'native keys');
|
||||
equal(JSON.stringify(Array.from(p.values.call(map))), '[[7,8]]', 'native values');
|
||||
const thisArg = {};
|
||||
let seen = 0;
|
||||
p.forEach.call(map, function(value, key, receiver) {
|
||||
equal(this, thisArg, 'forEach thisArg'); equal(receiver, map, 'forEach native receiver');
|
||||
equal(key, 'late', 'forEach key'); equal(JSON.stringify(value), '[7,8]', 'forEach value'); seen++;
|
||||
}, thisArg);
|
||||
equal(seen, 1, 'forEach called once');
|
||||
equal(rule.cssText.includes('late: 7 8'), true, 'map mutation updates native rule');
|
||||
equal(p.delete.call(map, 'late'), true, 'native delete');
|
||||
equal(p.delete.call(map, 'late'), false, 'native missing delete');
|
||||
}
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(map);
|
||||
Object.setPrototypeOf(map, null);
|
||||
try {
|
||||
prototypeFor(globalThis).set.call(map, 'branded', 9);
|
||||
equal(JSON.stringify(prototypeFor(globalThis).get.call(map, 'branded')), '[9]', 'brand survives prototype changes');
|
||||
} finally { Object.setPrototypeOf(map, prototype); }
|
||||
}
|
||||
return true;
|
||||
})()
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
const equal = (actual, expected, label) => {
|
||||
if (!Object.is(actual, expected)) throw new Error(`${label}: ${String(actual)} !== ${String(expected)}`);
|
||||
};
|
||||
const throwsTypeError = (run, realm, label) => {
|
||||
try { run(); } catch (error) {
|
||||
equal(Object.getPrototypeOf(error), realm.TypeError.prototype, `${label} error realm`);
|
||||
return;
|
||||
}
|
||||
throw new Error(`${label}: missing TypeError`);
|
||||
};
|
||||
const realms = [globalThis, document.getElementById('child').contentWindow];
|
||||
const properties = ['fontFamily', 'annotation', 'ornaments', 'stylistic', 'swash', 'characterVariant', 'styleset'];
|
||||
const records = [];
|
||||
for (const realm of realms) {
|
||||
for (const connected of [false, true]) {
|
||||
const text = '@font-feature-values Seed { @styleset { existing: 2 3; } } .other {}';
|
||||
const style = connected ? realm.document.createElement('style') : null;
|
||||
if (style) { style.textContent = text; realm.document.head.append(style); }
|
||||
const sheet = style ? style.sheet : new realm.CSSStyleSheet();
|
||||
if (!style) sheet.replaceSync(text);
|
||||
records.push({ realm, style, sheet, rule: sheet.cssRules[0], wrong: sheet.cssRules[1] });
|
||||
}
|
||||
}
|
||||
let conversions = 0, traps = 0;
|
||||
const value = { toString() { conversions++; return 'Converted'; } };
|
||||
const handler = {
|
||||
get() { traps++; throw new Error('author get trap'); },
|
||||
getPrototypeOf() { traps++; throw new Error('author prototype trap'); }
|
||||
};
|
||||
const revoked = Proxy.revocable(records[0].rule, handler); revoked.revoke();
|
||||
const invalid = [null, undefined, false, 1, 'text', Symbol(), 1n, {},
|
||||
new Map(), document.createElement('div'), CSSRule.prototype,
|
||||
{ cssText: records[0].rule.cssText, fontFamily: 'Seed' }, revoked.proxy];
|
||||
for (const { realm, rule, wrong } of records) {
|
||||
invalid.push(wrong, rule.styleset, realm.CSSFontFeatureValuesRule.prototype,
|
||||
Object.create(realm.CSSFontFeatureValuesRule.prototype), Object.create(rule), new Proxy(rule, handler));
|
||||
}
|
||||
for (const realm of realms) {
|
||||
for (const property of properties) {
|
||||
const get = Object.getOwnPropertyDescriptor(realm.CSSFontFeatureValuesRule.prototype, property).get;
|
||||
for (const receiver of invalid) throwsTypeError(() => get.call(receiver), realm, property);
|
||||
for (const { rule } of records) {
|
||||
const actual = get.call(rule), expected = rule[property];
|
||||
if (property === 'fontFamily') equal(actual, expected, `borrow ${property}`);
|
||||
else equal(JSON.stringify(Array.from(actual)), JSON.stringify(Array.from(expected)), `borrow ${property}`);
|
||||
}
|
||||
}
|
||||
const set = Object.getOwnPropertyDescriptor(realm.CSSFontFeatureValuesRule.prototype, 'fontFamily').set;
|
||||
for (const receiver of invalid) throwsTypeError(() => set.call(receiver, value), realm, 'fontFamily setter');
|
||||
}
|
||||
equal(conversions, 0, 'reject before DOMString conversion');
|
||||
equal(traps, 0, 'reject without Proxy traps');
|
||||
for (const { rule } of records) equal(rule.fontFamily, 'Seed', 'invalid receiver did not mutate rule');
|
||||
const marker = {};
|
||||
for (const realm of realms) {
|
||||
const set = Object.getOwnPropertyDescriptor(realm.CSSFontFeatureValuesRule.prototype, 'fontFamily').set;
|
||||
for (const { rule } of records) {
|
||||
let caught;
|
||||
try { set.call(rule, { toString() { throw marker; } }); } catch (error) { caught = error; }
|
||||
equal(caught, marker, 'genuine receiver propagates conversion exception');
|
||||
equal(rule.fontFamily, 'Seed', 'failed conversion leaves native rule intact');
|
||||
}
|
||||
}
|
||||
for (const { sheet, rule, style } of records) {
|
||||
const maps = properties.slice(1).map(property => rule[property]);
|
||||
const mapPrototype = Object.getPrototypeOf(maps[0]);
|
||||
for (const detached of [false, true]) {
|
||||
if (detached) sheet.deleteRule(0);
|
||||
for (const realm of realms) {
|
||||
const proto = realm.CSSFontFeatureValuesRule.prototype;
|
||||
const { get, set } = Object.getOwnPropertyDescriptor(proto, 'fontFamily');
|
||||
equal(set.call(rule, { toString() { conversions++; return 'Updated'; } }), undefined, 'setter return');
|
||||
equal(get.call(rule), 'Updated', 'native family after borrowed setter');
|
||||
properties.slice(1).forEach((property, index) => {
|
||||
const read = Object.getOwnPropertyDescriptor(proto, property).get;
|
||||
equal(JSON.stringify(Array.from(read.call(rule))), JSON.stringify(Array.from(maps[index])), 'retained feature values');
|
||||
equal(Object.getPrototypeOf(maps[index]), mapPrototype, 'retained map prototype');
|
||||
});
|
||||
}
|
||||
equal(rule.parentStyleSheet, detached ? null : sheet, 'rule lifecycle');
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(rule);
|
||||
Object.setPrototypeOf(rule, null);
|
||||
try {
|
||||
const { get, set } = Object.getOwnPropertyDescriptor(CSSFontFeatureValuesRule.prototype, 'fontFamily');
|
||||
set.call(rule, 'Branded');
|
||||
equal(get.call(rule), 'Branded', 'native brand survives prototype changes');
|
||||
} finally { Object.setPrototypeOf(rule, prototype); }
|
||||
if (style) style.remove();
|
||||
}
|
||||
equal(conversions, 16, 'each genuine setter converts once');
|
||||
return true;
|
||||
})()
|
||||
@@ -1,5 +1,31 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn css_font_feature_rule_receivers_use_native_brands_before_conversion() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://css-font-feature-receivers.test/",
|
||||
"<!doctype html><body><iframe id=child></iframe>",
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval(include_str!("css_font_feature_rule_receivers.js"))
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_font_feature_map_receivers_preserve_cross_realm_native_operations() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://css-font-feature-map-receivers.test/",
|
||||
"<!doctype html><body><iframe id=child></iframe>",
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval(include_str!("css_font_feature_map_receivers.js"))
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_font_face_rule_style_exposes_font_descriptors() {
|
||||
let mut vm = new_storage_test_vm("https://css-font-face-rule-style.test/");
|
||||
|
||||
Reference in New Issue
Block a user