diff --git a/moli-renderer-v8/src/context_bootstrap/canvas.rs b/moli-renderer-v8/src/context_bootstrap/canvas.rs index c62d3d8f6..288f5c427 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas.rs @@ -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); + } _ => {} } } diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs index f6e75dfeb..3e5ccca4b 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs @@ -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::(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> { + 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>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 4e92b9909..ef2392bf7 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -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> { + 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, diff --git a/moli-renderer-v8/src/native_bridge/mod.rs b/moli-renderer-v8/src/native_bridge/mod.rs index a1f117330..2fdfe7a6c 100644 --- a/moli-renderer-v8/src/native_bridge/mod.rs +++ b/moli-renderer-v8/src/native_bridge/mod.rs @@ -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}; diff --git a/moli-renderer-v8/src/native_bridge/node.rs b/moli-renderer-v8/src/native_bridge/node.rs index b745cb968..0fd1ef349 100644 --- a/moli-renderer-v8/src/native_bridge/node.rs +++ b/moli-renderer-v8/src/native_bridge/node.rs @@ -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> { + 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> { + 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, diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs index aeffe3b83..3464ef2ed 100644 --- a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs +++ b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs @@ -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/");