Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions moli-renderer-v8/src/context_bootstrap/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
_ => {}
}
}
54 changes: 51 additions & 3 deletions moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '_>,
Expand All @@ -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,
)]
Expand Down Expand Up @@ -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;
};
Expand All @@ -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>,
Expand Down
9 changes: 9 additions & 0 deletions moli-renderer-v8/src/native_bridge/context_host/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions moli-renderer-v8/src/native_bridge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
33 changes: 33 additions & 0 deletions moli-renderer-v8/src/native_bridge/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
79 changes: 79 additions & 0 deletions moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
Expand Down
Loading