mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 08:00:45 +00:00
fix(canvas): expose branded readonly TextMetrics width
This commit is contained in:
@@ -283,6 +283,9 @@ pub(super) fn install_canvas_template_bindings<'s>(
|
||||
"ImageBitmap" => {
|
||||
image_bitmap::install_image_bitmap_template_bindings(scope, template);
|
||||
}
|
||||
"TextMetrics" => {
|
||||
context2d::install_text_metrics_template_bindings(scope, template);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use std::str::FromStr;
|
||||
|
||||
const DEFAULT_IMAGE_SMOOTHING_QUALITY: &str = "low";
|
||||
const CANVAS_CONTEXT_LINE_DASH_SLOT: &str = "__moliCanvasContextLineDash";
|
||||
const TEXT_METRICS_WIDTH_SLOT: &str = "__moliTextMetricsWidth";
|
||||
|
||||
pub(super) fn reset_canvas_context_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
@@ -37,12 +38,41 @@ pub(super) fn reset_canvas_context_state<'s>(
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = "Object")]
|
||||
#[webapi(interface = "TextMetrics")]
|
||||
struct CanvasTextMetricsDeclaration {
|
||||
#[webapi(data_property)]
|
||||
#[webapi(slot = TEXT_METRICS_WIDTH_SLOT)]
|
||||
width: f64,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(name = "TextMetrics", enumerable)]
|
||||
struct TextMetricsPrototypeDeclaration {
|
||||
#[webapi(accessor_property, getter = text_metrics_width_getter)]
|
||||
width: (),
|
||||
}
|
||||
|
||||
pub(super) fn install_text_metrics_template_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
template: v8::Local<'s, v8::FunctionTemplate>,
|
||||
) {
|
||||
TextMetricsPrototypeDeclaration::initialize_prototype_template(
|
||||
scope,
|
||||
template.prototype_template(scope),
|
||||
);
|
||||
}
|
||||
|
||||
fn text_metrics_width_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(width) = get_private_value(scope, args.this(), TEXT_METRICS_WIDTH_SLOT) else {
|
||||
throw_type_error(scope, "TextMetrics.width called on incompatible receiver.");
|
||||
return;
|
||||
};
|
||||
rv.set(width);
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Eq, PartialEq, strum::EnumString, strum::IntoStaticStr, webidl::WebIdlEnum,
|
||||
)]
|
||||
@@ -1817,6 +1847,9 @@ pub(crate) fn canvas_context_measure_text_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if !require_canvas_context_receiver(scope, args.this(), "measureText") {
|
||||
return;
|
||||
}
|
||||
let Some(parsed) = webidl::parse_args::<CanvasContextMeasureTextArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
@@ -1825,12 +1858,27 @@ pub(crate) fn canvas_context_measure_text_callback<'s>(
|
||||
let declaration = CanvasTextMetricsDeclaration {
|
||||
width: measure_text_width(&parsed.text, &font),
|
||||
};
|
||||
let Ok(metrics) = declaration.bind(scope) else {
|
||||
let relevant_context = canvas_context_relevant_context(scope, args.this())
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
|
||||
let Ok(metrics) = declaration.bind(target_scope) else {
|
||||
return;
|
||||
};
|
||||
rv.set(metrics.into());
|
||||
}
|
||||
|
||||
fn canvas_context_relevant_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
context: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
canvas_owner_from_context(scope, context)
|
||||
.and_then(|canvas| {
|
||||
crate::context_bootstrap::shared::node_owner_document_or_self(scope, canvas)
|
||||
})
|
||||
.and_then(|document| crate::native_bridge::node_relevant_context(scope, document))
|
||||
.or_else(|| context.get_creation_context(scope))
|
||||
}
|
||||
|
||||
pub(crate) fn canvas_context_create_image_data_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
|
||||
@@ -2088,6 +2088,15 @@ impl JsContextHost {
|
||||
constructor.get(scope, crate::util::v8str(scope, "prototype").into())
|
||||
}
|
||||
|
||||
pub(crate) fn child_browsing_context_relevant_context<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
child_handle: DomHandle,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
let window = self.existing_child_browsing_context_window_wrapper(scope, child_handle)?;
|
||||
window.get_creation_context(scope)
|
||||
}
|
||||
|
||||
pub(crate) fn custom_elements_for_registry_key(
|
||||
&self,
|
||||
key: CustomElementRegistryKey,
|
||||
|
||||
@@ -77,8 +77,9 @@ pub(crate) use element::{
|
||||
pub(super) use helpers::*;
|
||||
pub(crate) use node::{
|
||||
current_or_live_delegate_node_arg_handle, node_or_foreign_arg_handle_allow_detached,
|
||||
node_runtime_and_handle_from_object, node_runtime_and_handle_from_object_or_detached,
|
||||
object_is_node_wrapper_or_detached, validate_pre_insert_handles,
|
||||
node_relevant_context, node_runtime_and_handle_from_object,
|
||||
node_runtime_and_handle_from_object_or_detached, object_is_node_wrapper_or_detached,
|
||||
validate_pre_insert_handles,
|
||||
};
|
||||
pub(crate) use node::{install_character_data_template_bindings, install_node_template_bindings};
|
||||
|
||||
|
||||
@@ -1307,6 +1307,39 @@ pub(super) fn node_runtime_and_handle_from_args_or_detached(
|
||||
node_runtime_and_handle_from_object_or_detached(scope, this)
|
||||
}
|
||||
|
||||
pub(super) fn node_owner_document_relevant_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
let document_handle = unsafe { &*runtime_ptr }
|
||||
.dom_host()
|
||||
.owner_document_handle(handle)?;
|
||||
if let Some(child_handle) =
|
||||
unsafe { &*runtime_ptr }.child_browsing_context_host_for_document_handle(document_handle)
|
||||
&& let Some(context) =
|
||||
unsafe { &*runtime_ptr }.child_browsing_context_relevant_context(scope, child_handle)
|
||||
{
|
||||
return Some(context);
|
||||
}
|
||||
let document = unsafe { &mut *runtime_ptr }
|
||||
.native_bridge_mut()
|
||||
.wrap_handle(scope, runtime_ptr, document_handle)?;
|
||||
document.get_creation_context(scope)
|
||||
}
|
||||
|
||||
pub(crate) fn node_relevant_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
node_runtime_and_handle_from_object(scope, object)
|
||||
.ok()
|
||||
.and_then(|(runtime_ptr, handle)| {
|
||||
node_owner_document_relevant_context(scope, runtime_ptr, handle)
|
||||
})
|
||||
.or_else(|| object.get_creation_context(scope))
|
||||
}
|
||||
|
||||
pub(super) fn node_arg_handle(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
|
||||
@@ -580,6 +580,85 @@ fn html_canvas_2d_text_methods_are_available_for_fingerprinting_scripts() {
|
||||
assert_eq!(result, "function|function|function|function|true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_text_metrics_width_is_a_branded_readonly_snapshot() {
|
||||
let mut vm = new_storage_test_vm("https://text-metrics.test/");
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const contexts = [
|
||||
document.createElement('canvas').getContext('2d'),
|
||||
new OffscreenCanvas(1, 1).getContext('2d')
|
||||
];
|
||||
const descriptor = Object.getOwnPropertyDescriptor(TextMetrics.prototype, 'width');
|
||||
const outcome = callback => {
|
||||
try { callback(); return 'ok'; } catch (error) { return error.name; }
|
||||
};
|
||||
return JSON.stringify({
|
||||
descriptor: [descriptor.get.name, descriptor.get.length,
|
||||
descriptor.set === undefined, descriptor.enumerable, descriptor.configurable],
|
||||
constructor: outcome(() => new TextMetrics()),
|
||||
results: contexts.map(context => {
|
||||
context.font = '10px Arial';
|
||||
const metrics = context.measureText('Hello');
|
||||
const width = metrics.width;
|
||||
const ownNames = Object.getOwnPropertyNames(metrics);
|
||||
metrics.width = -1;
|
||||
metrics.__moliTextMetricsWidth = -2;
|
||||
context.font = '20px Arial';
|
||||
return [
|
||||
metrics instanceof TextMetrics,
|
||||
Object.prototype.toString.call(metrics),
|
||||
ownNames.length, width > 0, metrics.width === width,
|
||||
context.measureText('Hello').width > width,
|
||||
context.measureText('').width,
|
||||
outcome(() => { 'use strict'; metrics.width = 42; }),
|
||||
outcome(() => descriptor.get.call(Object.create(metrics))),
|
||||
outcome(() => context.measureText.call({}, 'Hello'))
|
||||
];
|
||||
}),
|
||||
fakeReceivers: [null, undefined, {}, TextMetrics.prototype,
|
||||
{ __moliTextMetricsWidth: 1 }].map(value => outcome(() => descriptor.get.call(value)))
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("TextMetrics width contract should evaluate");
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"descriptor":["get width",0,true,true,true],"constructor":"TypeError","results":[[true,"[object TextMetrics]",0,true,true,true,0,"TypeError","TypeError","TypeError"],[true,"[object TextMetrics]",0,true,true,true,0,"TypeError","TypeError","TypeError"]],"fakeReceivers":["TypeError","TypeError","TypeError","TypeError","TypeError"]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_text_metrics_uses_the_context_realm() {
|
||||
let mut vm = new_storage_test_vm("https://text-metrics-realm.test/");
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
document.appendChild(document.createElement('html')).appendChild(frame);
|
||||
const child = frame.contentWindow;
|
||||
const localContext = document.createElement('canvas').getContext('2d');
|
||||
const childContext = child.document.createElement('canvas').getContext('2d');
|
||||
const local = childContext.measureText.call(localContext, 'Hello');
|
||||
const foreign = localContext.measureText.call(childContext, 'Hello');
|
||||
const getter = Object.getOwnPropertyDescriptor(TextMetrics.prototype, 'width').get;
|
||||
return [
|
||||
Object.getPrototypeOf(local) === TextMetrics.prototype,
|
||||
Object.getPrototypeOf(foreign) === child.TextMetrics.prototype,
|
||||
getter.call(foreign) === foreign.width,
|
||||
foreign.width > 0
|
||||
].join('|');
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("TextMetrics should use the canvas context's realm");
|
||||
assert_eq!(result, "true|true|true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_canvas_linear_gradient_surface_is_available_for_fingerprinting_scripts() {
|
||||
let mut vm = new_storage_test_vm("https://canvas-linear-gradient-surface.test/");
|
||||
|
||||
Reference in New Issue
Block a user