diff options
Diffstat (limited to 'components')
228 files changed, 5232 insertions, 2334 deletions
diff --git a/components/canvas/backend.rs b/components/canvas/backend.rs index 7e348fbc9b9..b7296b81ba3 100644 --- a/components/canvas/backend.rs +++ b/components/canvas/backend.rs @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::borrow::Cow; + use canvas_traits::canvas::{ CompositionOrBlending, FillOrStrokeStyle, LineCapStyle, LineJoinStyle, }; @@ -21,7 +23,6 @@ pub(crate) trait Backend: Clone + Sized { type DrawTarget: GenericDrawTarget<Self>; type PathBuilder: GenericPathBuilder<Self>; type SourceSurface; - type Bytes<'a>: AsRef<[u8]>; type Path: PathHelpers<Self> + Clone; type GradientStop; type GradientStops; @@ -122,7 +123,7 @@ pub(crate) trait GenericDrawTarget<B: Backend> { draw_options: &B::DrawOptions, ); fn surface(&self) -> B::SourceSurface; - fn bytes(&'_ self) -> B::Bytes<'_>; + fn bytes(&self) -> Cow<[u8]>; } /// A generic PathBuilder that abstracts the interface for azure's and raqote's PathBuilder. diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index ea30589d0af..90b26df1cda 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -154,7 +154,18 @@ struct PathBuilderRef<'a, B: Backend> { } impl<B: Backend> PathBuilderRef<'_, B> { + /// <https://html.spec.whatwg.org/multipage#ensure-there-is-a-subpath> + fn ensure_there_is_a_subpath(&mut self, point: &Point2D<f32>) { + if self.builder.get_current_point().is_none() { + self.builder.move_to(*point); + } + } + + /// <https://html.spec.whatwg.org/multipage#dom-context-2d-lineto> fn line_to(&mut self, pt: &Point2D<f32>) { + // 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y). + self.ensure_there_is_a_subpath(pt); + let pt = self.transform.transform_point(*pt); self.builder.line_to(pt); } @@ -182,14 +193,22 @@ impl<B: Backend> PathBuilderRef<'_, B> { self.move_to(&first); } + /// <https://html.spec.whatwg.org/multipage#dom-context-2d-quadraticcurveto> fn quadratic_curve_to(&mut self, cp: &Point2D<f32>, endpoint: &Point2D<f32>) { + // 2. Ensure there is a subpath for (cpx, cpy). + self.ensure_there_is_a_subpath(cp); + self.builder.quadratic_curve_to( &self.transform.transform_point(*cp), &self.transform.transform_point(*endpoint), ) } + /// <https://html.spec.whatwg.org/multipage#dom-context-2d-beziercurveto> fn bezier_curve_to(&mut self, cp1: &Point2D<f32>, cp2: &Point2D<f32>, endpoint: &Point2D<f32>) { + // 2. Ensure there is a subpath for (cp1x, cp1y). + self.ensure_there_is_a_subpath(cp1); + self.builder.bezier_curve_to( &self.transform.transform_point(*cp1), &self.transform.transform_point(*cp2), @@ -210,6 +229,7 @@ impl<B: Backend> PathBuilderRef<'_, B> { .arc(center, radius, start_angle, end_angle, ccw); } + /// <https://html.spec.whatwg.org/multipage#dom-context-2d-arcto> fn arc_to(&mut self, cp1: &Point2D<f32>, cp2: &Point2D<f32>, radius: f32) { let cp0 = if let (Some(inverse), Some(point)) = (self.transform.inverse(), self.builder.get_current_point()) @@ -218,6 +238,9 @@ impl<B: Backend> PathBuilderRef<'_, B> { } else { *cp1 }; + + // 2. Ensure there is a subpath for (x1, y1) is done by one of self.line_to calls + if (cp0.x == cp1.x && cp0.y == cp1.y) || cp1 == cp2 || radius == 0.0 { self.line_to(cp1); return; @@ -447,7 +470,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { pub(crate) fn draw_image( &mut self, image_data: &[u8], - image_size: Size2D<u64>, + image_size: Size2D<u32>, dest_rect: Rect<f64>, source_rect: Rect<f64>, smoothing_enabled: bool, @@ -457,7 +480,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { let source_rect = source_rect.ceil(); // It discards the extra pixels (if any) that won't be painted let image_data = if Rect::from_size(image_size.to_f64()).contains_rect(&source_rect) { - pixels::rgba8_get_rect(image_data, image_size, source_rect.to_u64()).into() + pixels::rgba8_get_rect(image_data, image_size, source_rect.to_u32()).into() } else { image_data.into() }; @@ -1221,7 +1244,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { } // https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata - pub(crate) fn put_image_data(&mut self, mut imagedata: Vec<u8>, rect: Rect<u64>) { + pub(crate) fn put_image_data(&mut self, mut imagedata: Vec<u8>, rect: Rect<u32>) { assert_eq!(imagedata.len() % 4, 0); assert_eq!(rect.size.area() as usize, imagedata.len() / 4); pixels::rgba8_byte_swap_and_premultiply_inplace(&mut imagedata); @@ -1310,8 +1333,8 @@ impl<'a, B: Backend> CanvasData<'a, B> { #[allow(unsafe_code)] pub(crate) fn read_pixels( &self, - read_rect: Option<Rect<u64>>, - canvas_size: Option<Size2D<u64>>, + read_rect: Option<Rect<u32>>, + canvas_size: Option<Size2D<u32>>, ) -> Snapshot { let canvas_size = canvas_size.unwrap_or(self.drawtarget.get_size().cast()); @@ -1327,7 +1350,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { .to_vec() } } else { - self.drawtarget.bytes().as_ref().to_vec() + self.drawtarget.bytes().into_owned() }; Snapshot::from_vec( diff --git a/components/canvas/canvas_paint_thread.rs b/components/canvas/canvas_paint_thread.rs index 82a221d560d..93aff081c35 100644 --- a/components/canvas/canvas_paint_thread.rs +++ b/components/canvas/canvas_paint_thread.rs @@ -97,7 +97,10 @@ impl<'a> CanvasPaintThread<'a> { let canvas_data = canvas_paint_thread.create_canvas(size); creator.send(canvas_data).unwrap(); }, - Ok(ConstellationCanvasMsg::Exit) => break, + Ok(ConstellationCanvasMsg::Exit(exit_sender)) => { + let _ = exit_sender.send(()); + break; + }, Err(e) => { warn!("Error on CanvasPaintThread receive ({})", e); break; @@ -184,7 +187,7 @@ impl<'a> CanvasPaintThread<'a> { Canvas2dMsg::DrawEmptyImage(image_size, dest_rect, source_rect) => { self.canvas(canvas_id).draw_image( &vec![0; image_size.area() as usize * 4], - image_size.to_u64(), + image_size, dest_rect, source_rect, false, @@ -200,10 +203,10 @@ impl<'a> CanvasPaintThread<'a> { ) => { let image_data = self .canvas(canvas_id) - .read_pixels(Some(source_rect.to_u64()), Some(image_size.to_u64())); + .read_pixels(Some(source_rect.to_u32()), Some(image_size)); self.canvas(other_canvas_id).draw_image( image_data.data(), - source_rect.size.to_u64(), + source_rect.size.to_u32(), dest_rect, source_rect, smoothing, @@ -395,7 +398,7 @@ impl Canvas<'_> { fn draw_image( &mut self, data: &[u8], - size: Size2D<u64>, + size: Size2D<u32>, dest_rect: Rect<f64>, source_rect: Rect<f64>, smoothing_enabled: bool, @@ -415,8 +418,8 @@ impl Canvas<'_> { fn read_pixels( &mut self, - read_rect: Option<Rect<u64>>, - canvas_size: Option<Size2D<u64>>, + read_rect: Option<Rect<u32>>, + canvas_size: Option<Size2D<u32>>, ) -> snapshot::Snapshot { match self { Canvas::Raqote(canvas_data) => canvas_data.read_pixels(read_rect, canvas_size), @@ -609,7 +612,7 @@ impl Canvas<'_> { } } - fn put_image_data(&mut self, unwrap: Vec<u8>, rect: Rect<u64>) { + fn put_image_data(&mut self, unwrap: Vec<u8>, rect: Rect<u32>) { match self { Canvas::Raqote(canvas_data) => canvas_data.put_image_data(unwrap, rect), } diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index ecf780c36d5..02d87dfcd54 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; @@ -41,7 +42,6 @@ impl Backend for RaqoteBackend { type DrawTarget = raqote::DrawTarget; type PathBuilder = PathBuilder; type SourceSurface = Vec<u8>; // TODO: See if we can avoid the alloc (probably?) - type Bytes<'a> = &'a [u8]; type Path = raqote::Path; type GradientStop = raqote::GradientStop; type GradientStops = Vec<raqote::GradientStop>; @@ -656,9 +656,11 @@ impl GenericDrawTarget<RaqoteBackend> for raqote::DrawTarget { ); } #[allow(unsafe_code)] - fn bytes(&self) -> &[u8] { + fn bytes(&self) -> Cow<[u8]> { let v = self.get_data(); - unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) } + Cow::Borrowed(unsafe { + std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) + }) } } @@ -708,7 +710,7 @@ impl GenericPathBuilder<RaqoteBackend> for PathBuilder { PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)), PathOp::CubicTo(_, _, point) => Some(Point2D::new(point.x, point.y)), PathOp::QuadTo(_, point) => Some(Point2D::new(point.x, point.y)), - PathOp::Close => None, + PathOp::Close => path.ops.first().and_then(get_first_point), }) } @@ -731,6 +733,15 @@ impl GenericPathBuilder<RaqoteBackend> for PathBuilder { } } +fn get_first_point(op: &PathOp) -> Option<euclid::Point2D<f32, euclid::UnknownUnit>> { + match op { + PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)), + PathOp::CubicTo(point, _, _) => Some(Point2D::new(point.x, point.y)), + PathOp::QuadTo(point, _) => Some(Point2D::new(point.x, point.y)), + PathOp::Close => None, + } +} + pub trait ToRaqoteStyle { type Target; diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index 9b4ff54d29e..084bb54a5fc 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -38,6 +38,7 @@ servo_allocator = { path = "../allocator" } servo_config = { path = "../config" } servo_geometry = { path = "../geometry" } stylo_traits = { workspace = true } +timers = { path = "../timers" } tracing = { workspace = true, optional = true } webrender = { workspace = true } webrender_api = { workspace = true } diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 27d1e8f93e1..b598ed939f9 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -2,14 +2,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::{Cell, RefCell}; +use std::cell::{Cell, Ref, RefCell}; use std::collections::HashMap; use std::env; use std::fs::create_dir_all; use std::iter::once; use std::rc::Rc; use std::sync::Arc; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; use base::cross_process_instant::CrossProcessInstant; use base::id::{PipelineId, WebViewId}; @@ -53,6 +53,7 @@ use webrender_api::{ }; use crate::InitialCompositorState; +use crate::refresh_driver::RefreshDriver; use crate::webview_manager::WebViewManager; use crate::webview_renderer::{PinchZoomResult, UnknownWebView, WebViewRenderer}; @@ -86,6 +87,9 @@ pub enum WebRenderDebugOption { } /// Data that is shared by all WebView renderers. pub struct ServoRenderer { + /// The [`RefreshDriver`] which manages the rythym of painting. + refresh_driver: RefreshDriver, + /// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once /// all renderer operations become per-`WebView` this map can be removed, but we still sometimes /// need to work backwards to figure out what `WebView` is associated with a `Pipeline`. @@ -151,18 +155,14 @@ pub struct IOCompositor { /// The number of frames pending to receive from WebRender. pending_frames: usize, - /// The [`Instant`] of the last animation tick, used to avoid flooding the Constellation and - /// ScriptThread with a deluge of animation ticks. - last_animation_tick: Instant, - /// A handle to the memory profiler which will automatically unregister /// when it's dropped. _mem_profiler_registration: ProfilerRegistration, } /// Why we need to be repainted. This is used for debugging. -#[derive(Clone, Copy, Default)] -struct RepaintReason(u8); +#[derive(Clone, Copy, Default, PartialEq)] +pub(crate) struct RepaintReason(u8); bitflags! { impl RepaintReason: u8 { @@ -281,6 +281,11 @@ impl PipelineDetails { } } +pub enum HitTestError { + EpochMismatch, + Others, +} + impl ServoRenderer { pub fn shutdown_state(&self) -> ShutdownState { self.shutdown_state.get() @@ -290,15 +295,19 @@ impl ServoRenderer { &self, point: DevicePoint, details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, - ) -> Option<CompositorHitTestResult> { - self.hit_test_at_point_with_flags_and_pipeline( + ) -> Result<CompositorHitTestResult, HitTestError> { + match self.hit_test_at_point_with_flags_and_pipeline( point, HitTestFlags::empty(), None, details_for_pipeline, - ) - .first() - .cloned() + ) { + Ok(hit_test_results) => hit_test_results + .first() + .cloned() + .ok_or(HitTestError::Others), + Err(error) => Err(error), + } } // TODO: split this into first half (global) and second half (one for whole compositor, one for webview) @@ -308,14 +317,15 @@ impl ServoRenderer { flags: HitTestFlags, pipeline_id: Option<WebRenderPipelineId>, details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, - ) -> Vec<CompositorHitTestResult> { + ) -> Result<Vec<CompositorHitTestResult>, HitTestError> { // DevicePoint and WorldPoint are the same for us. let world_point = WorldPoint::from_untyped(point.to_untyped()); let results = self.webrender_api .hit_test(self.webrender_document, pipeline_id, world_point, flags); - results + let mut epoch_mismatch = false; + let results = results .items .iter() .filter_map(|item| { @@ -323,10 +333,16 @@ impl ServoRenderer { let details = details_for_pipeline(pipeline_id)?; // If the epoch in the tag does not match the current epoch of the pipeline, - // then the hit test is against an old version of the display list and we - // should ignore this hit test for now. + // then the hit test is against an old version of the display list. match details.most_recent_display_list_epoch { - Some(epoch) if epoch.as_u16() == item.tag.1 => {}, + Some(epoch) => { + if epoch.as_u16() != item.tag.1 { + // It's too early to hit test for now. + // New scene building is in progress. + epoch_mismatch = true; + return None; + } + }, _ => return None, } @@ -340,7 +356,13 @@ impl ServoRenderer { scroll_tree_node: info.scroll_tree_node, }) }) - .collect() + .collect(); + + if epoch_mismatch { + return Err(HitTestError::EpochMismatch); + } + + Ok(results) } pub(crate) fn send_transaction(&mut self, transaction: Transaction) { @@ -386,6 +408,10 @@ impl IOCompositor { ); let compositor = IOCompositor { global: Rc::new(RefCell::new(ServoRenderer { + refresh_driver: RefreshDriver::new( + state.constellation_chan.clone(), + state.event_loop_waker, + ), shutdown_state: state.shutdown_state, pipeline_to_webview_map: Default::default(), compositor_receiver: state.receiver, @@ -406,7 +432,6 @@ impl IOCompositor { webrender: Some(state.webrender), rendering_context: state.rendering_context, pending_frames: 0, - last_animation_tick: Instant::now(), _mem_profiler_registration: registration, }; @@ -450,7 +475,16 @@ impl IOCompositor { } pub fn needs_repaint(&self) -> bool { - !self.needs_repaint.get().is_empty() + let repaint_reason = self.needs_repaint.get(); + if repaint_reason.is_empty() { + return false; + } + + !self + .global + .borrow() + .refresh_driver + .wait_to_paint(repaint_reason) } pub fn finish_shutting_down(&mut self) { @@ -519,15 +553,17 @@ impl IOCompositor { pipeline_id, animation_state, ) => { - if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { - if webview_renderer - .change_pipeline_running_animations_state(pipeline_id, animation_state) && - webview_renderer.animating() - { - // These operations should eventually happen per-WebView, but they are - // global now as rendering is still global to all WebViews. - self.process_animations(true); - } + let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { + return; + }; + + if webview_renderer + .change_pipeline_running_animations_state(pipeline_id, animation_state) + { + self.global + .borrow() + .refresh_driver + .notify_animation_state_changed(webview_renderer); } }, @@ -572,14 +608,15 @@ impl IOCompositor { }, CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => { - if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { - if webview_renderer.set_throttled(pipeline_id, throttled) && - webview_renderer.animating() - { - // These operations should eventually happen per-WebView, but they are - // global now as rendering is still global to all WebViews. - self.process_animations(true); - } + let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { + return; + }; + + if webview_renderer.set_throttled(pipeline_id, throttled) { + self.global + .borrow() + .refresh_driver + .notify_animation_state_changed(webview_renderer); } }, @@ -604,7 +641,7 @@ impl IOCompositor { .global .borrow() .hit_test_at_point(point, details_for_pipeline); - if let Some(result) = result { + if let Ok(result) = result { self.global.borrow_mut().update_cursor(point, &result); } } @@ -634,9 +671,9 @@ impl IOCompositor { }; let dppx = webview_renderer.device_pixels_per_page_pixel(); let point = dppx.transform_point(Point2D::new(x, y)); - webview_renderer.dispatch_input_event( + webview_renderer.dispatch_point_input_event( InputEvent::MouseButton(MouseButtonEvent::new(action, button, point)) - .with_webdriver_message_id(Some(message_id)), + .with_webdriver_message_id(message_id), ); }, @@ -647,13 +684,20 @@ impl IOCompositor { }; let dppx = webview_renderer.device_pixels_per_page_pixel(); let point = dppx.transform_point(Point2D::new(x, y)); - webview_renderer.dispatch_input_event( + webview_renderer.dispatch_point_input_event( InputEvent::MouseMove(MouseMoveEvent::new(point)) - .with_webdriver_message_id(Some(message_id)), + .with_webdriver_message_id(message_id), ); }, - CompositorMsg::WebDriverWheelScrollEvent(webview_id, x, y, delta_x, delta_y) => { + CompositorMsg::WebDriverWheelScrollEvent( + webview_id, + x, + y, + delta_x, + delta_y, + message_id, + ) => { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { warn!("Handling input event for unknown webview: {webview_id}"); return; @@ -668,8 +712,10 @@ impl IOCompositor { let point = dppx.transform_point(Point2D::new(x, y)); let scroll_delta = dppx.transform_vector(Vector2D::new(delta_x as f32, delta_y as f32)); - webview_renderer - .dispatch_input_event(InputEvent::Wheel(WheelEvent { delta, point })); + webview_renderer.dispatch_point_input_event( + InputEvent::Wheel(WheelEvent::new(delta, point)) + .with_webdriver_message_id(message_id), + ); webview_renderer.on_webdriver_wheel_action(scroll_delta, point); }, @@ -777,6 +823,8 @@ impl IOCompositor { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { return warn!("Could not find WebView for incoming display list"); }; + // WebRender is not ready until we receive "NewWebRenderFrameReady" + webview_renderer.webrender_frame_ready.set(false); let pipeline_id = display_list_info.pipeline_id; let details = webview_renderer.ensure_pipeline_details(pipeline_id.into()); @@ -826,7 +874,8 @@ impl IOCompositor { flags, pipeline, details_for_pipeline, - ); + ) + .unwrap_or_default(); let _ = sender.send(result); }, @@ -933,6 +982,11 @@ impl IOCompositor { warn!("Sending response to get screen size failed ({error:?})."); } }, + CompositorMsg::Viewport(webview_id, viewport_description) => { + if let Some(webview) = self.webview_renderers.get_mut(webview_id) { + webview.set_viewport_description(viewport_description); + } + }, } } @@ -1271,39 +1325,6 @@ impl IOCompositor { self.set_needs_repaint(RepaintReason::Resize); } - /// If there are any animations running, dispatches appropriate messages to the constellation. - fn process_animations(&mut self, force: bool) { - // When running animations in order to dump a screenshot (not after a full composite), don't send - // animation ticks faster than about 60Hz. - // - // TODO: This should be based on the refresh rate of the screen and also apply to all - // animation ticks, not just ones sent while waiting to dump screenshots. This requires - // something like a refresh driver concept though. - if !force && (Instant::now() - self.last_animation_tick) < Duration::from_millis(16) { - return; - } - self.last_animation_tick = Instant::now(); - - let animating_webviews: Vec<_> = self - .webview_renderers - .iter() - .filter_map(|webview_renderer| { - if webview_renderer.animating() { - Some(webview_renderer.id) - } else { - None - } - }) - .collect(); - if !animating_webviews.is_empty() { - if let Err(error) = self.global.borrow().constellation_sender.send( - EmbedderToConstellationMessage::TickAnimation(animating_webviews), - ) { - warn!("Sending tick to constellation failed ({error:?})."); - } - } - } - pub fn on_zoom_reset_window_event(&mut self, webview_id: WebViewId) { if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { return; @@ -1410,6 +1431,11 @@ impl IOCompositor { /// Render the WebRender scene to the active `RenderingContext`. If successful, trigger /// the next round of animations. pub fn render(&mut self) -> bool { + self.global + .borrow() + .refresh_driver + .notify_will_paint(self.webview_renderers.iter()); + if let Err(error) = self.render_inner() { warn!("Unable to render: {error:?}"); return false; @@ -1419,9 +1445,6 @@ impl IOCompositor { // the scene no longer needs to be repainted. self.needs_repaint.set(RepaintReason::empty()); - // Queue up any subsequent paints for animations. - self.process_animations(true); - true } @@ -1492,10 +1515,8 @@ impl IOCompositor { if opts::get().wait_for_stable_image { // The current image may be ready to output. However, if there are animations active, - // tick those instead and continue waiting for the image output to be stable AND - // all active animations to complete. + // continue waiting for the image output to be stable AND all active animations to complete. if self.animations_or_animation_callbacks_running() { - self.process_animations(false); return Err(UnableToComposite::NotReadyToPaintImage( NotReadyToPaint::AnimationsActive, )); @@ -1630,31 +1651,45 @@ impl IOCompositor { ); } + /// Get the message receiver for this [`IOCompositor`]. + pub fn receiver(&self) -> Ref<Receiver<CompositorMsg>> { + Ref::map(self.global.borrow(), |global| &global.compositor_receiver) + } + #[cfg_attr( feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - pub fn receive_messages(&mut self) { + pub fn handle_messages(&mut self, mut messages: Vec<CompositorMsg>) { // Check for new messages coming from the other threads in the system. - let mut compositor_messages = vec![]; let mut found_recomposite_msg = false; - while let Ok(msg) = self.global.borrow_mut().compositor_receiver.try_recv() { - match msg { + messages.retain(|message| { + match message { CompositorMsg::NewWebRenderFrameReady(..) if found_recomposite_msg => { // Only take one of duplicate NewWebRendeFrameReady messages, but do subtract // one frame from the pending frames. self.pending_frames -= 1; + false }, CompositorMsg::NewWebRenderFrameReady(..) => { found_recomposite_msg = true; - compositor_messages.push(msg) + + // Process all pending events + // FIXME: Shouldn't `webview_frame_ready` be stored globally and why can't `pending_frames` + // be used here? + self.webview_renderers.iter().for_each(|webview| { + webview.dispatch_pending_point_input_events(); + webview.webrender_frame_ready.set(true); + }); + + true }, - _ => compositor_messages.push(msg), + _ => true, } - } - for msg in compositor_messages { - self.handle_browser_message(msg); + }); + for message in messages { + self.handle_browser_message(message); if self.global.borrow().shutdown_state() == ShutdownState::FinishedShuttingDown { return; } diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index a66c61499e5..4faeadf5ba6 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -11,7 +11,7 @@ use compositing_traits::rendering_context::RenderingContext; use compositing_traits::{CompositorMsg, CompositorProxy}; use constellation_traits::EmbedderToConstellationMessage; use crossbeam_channel::{Receiver, Sender}; -use embedder_traits::ShutdownState; +use embedder_traits::{EventLoopWaker, ShutdownState}; use profile_traits::{mem, time}; use webrender::RenderApi; use webrender_api::DocumentId; @@ -22,9 +22,10 @@ pub use crate::compositor::{IOCompositor, WebRenderDebugOption}; mod tracing; mod compositor; +mod refresh_driver; mod touch; -pub mod webview_manager; -pub mod webview_renderer; +mod webview_manager; +mod webview_renderer; /// Data used to construct a compositor. pub struct InitialCompositorState { @@ -49,4 +50,7 @@ pub struct InitialCompositorState { pub webrender_gl: Rc<dyn gleam::gl::Gl>, #[cfg(feature = "webxr")] pub webxr_main_thread: webxr::MainThreadRegistry, + /// An [`EventLoopWaker`] used in order to wake up the embedder when it is + /// time to paint. + pub event_loop_waker: Box<dyn EventLoopWaker>, } diff --git a/components/compositing/refresh_driver.rs b/components/compositing/refresh_driver.rs new file mode 100644 index 00000000000..5531a7257c8 --- /dev/null +++ b/components/compositing/refresh_driver.rs @@ -0,0 +1,234 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::Cell; +use std::collections::hash_map::Values; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use base::id::WebViewId; +use constellation_traits::EmbedderToConstellationMessage; +use crossbeam_channel::{Sender, select}; +use embedder_traits::EventLoopWaker; +use log::warn; +use timers::{BoxedTimerCallback, TimerEventId, TimerEventRequest, TimerScheduler, TimerSource}; + +use crate::compositor::RepaintReason; +use crate::webview_renderer::WebViewRenderer; + +const FRAME_DURATION: Duration = Duration::from_millis(1000 / 120); + +/// The [`RefreshDriver`] is responsible for controlling updates to aall `WebView`s +/// onscreen presentation. Currently, it only manages controlling animation update +/// requests. +/// +/// The implementation is very basic at the moment, only requesting new animation +/// frames at a constant time after a repaint. +pub(crate) struct RefreshDriver { + /// The channel on which messages can be sent to the Constellation. + pub(crate) constellation_sender: Sender<EmbedderToConstellationMessage>, + + /// Whether or not we are currently animating via a timer. + pub(crate) animating: Cell<bool>, + + /// Whether or not we are waiting for our frame timeout to trigger + pub(crate) waiting_for_frame_timeout: Arc<AtomicBool>, + + /// A [`TimerThread`] which is used to schedule frame timeouts in the future. + timer_thread: TimerThread, + + /// An [`EventLoopWaker`] to be used to wake up the embedder when it is + /// time to paint a frame. + event_loop_waker: Box<dyn EventLoopWaker>, +} + +impl RefreshDriver { + pub(crate) fn new( + constellation_sender: Sender<EmbedderToConstellationMessage>, + event_loop_waker: Box<dyn EventLoopWaker>, + ) -> Self { + Self { + constellation_sender, + animating: Default::default(), + waiting_for_frame_timeout: Default::default(), + timer_thread: Default::default(), + event_loop_waker, + } + } + + fn timer_callback(&self) -> BoxedTimerCallback { + let waiting_for_frame_timeout = self.waiting_for_frame_timeout.clone(); + let event_loop_waker = self.event_loop_waker.clone_box(); + Box::new(move |_| { + waiting_for_frame_timeout.store(false, Ordering::Relaxed); + event_loop_waker.wake(); + }) + } + + /// Notify the [`RefreshDriver`] that a paint is about to happen. This will trigger + /// new animation frames for all active `WebView`s and schedule a new frame deadline. + pub(crate) fn notify_will_paint( + &self, + webview_renderers: Values<'_, WebViewId, WebViewRenderer>, + ) { + // If we are still waiting for the frame to timeout this paint was caused for some + // non-animation related reason and we should wait until the frame timeout to trigger + // the next one. + if self.waiting_for_frame_timeout.load(Ordering::Relaxed) { + return; + } + + // If any WebViews are animating ask them to paint again for another animation tick. + let animating_webviews: Vec<_> = webview_renderers + .filter_map(|webview_renderer| { + if webview_renderer.animating() { + Some(webview_renderer.id) + } else { + None + } + }) + .collect(); + + // If nothing is animating any longer, update our state and exit early without requesting + // any noew frames nor triggering a new animation deadline. + if animating_webviews.is_empty() { + self.animating.set(false); + return; + } + + if let Err(error) = + self.constellation_sender + .send(EmbedderToConstellationMessage::TickAnimation( + animating_webviews, + )) + { + warn!("Sending tick to constellation failed ({error:?})."); + } + + // Queue the next frame deadline. + self.animating.set(true); + self.waiting_for_frame_timeout + .store(true, Ordering::Relaxed); + self.timer_thread + .queue_timer(FRAME_DURATION, self.timer_callback()); + } + + /// Notify the [`RefreshDriver`] that the animation state of a particular `WebView` + /// via its associated [`WebViewRenderer`] has changed. In the case that a `WebView` + /// has started animating, the [`RefreshDriver`] will request a new frame from it + /// immediately, but only render that frame at the next frame deadline. + pub(crate) fn notify_animation_state_changed(&self, webview_renderer: &WebViewRenderer) { + if !webview_renderer.animating() { + // If no other WebView is animating we will officially stop animated once the + // next frame has been painted. + return; + } + + if let Err(error) = + self.constellation_sender + .send(EmbedderToConstellationMessage::TickAnimation(vec![ + webview_renderer.id, + ])) + { + warn!("Sending tick to constellation failed ({error:?})."); + } + + if self.animating.get() { + return; + } + + self.animating.set(true); + self.waiting_for_frame_timeout + .store(true, Ordering::Relaxed); + self.timer_thread + .queue_timer(FRAME_DURATION, self.timer_callback()); + } + + /// Whether or not the renderer should trigger a message to the embedder to request a + /// repaint. This might be false if: we are animating and the repaint reason is just + /// for a new frame. In that case, the renderer should wait until the frame timeout to + /// ask the embedder to repaint. + pub(crate) fn wait_to_paint(&self, repaint_reason: RepaintReason) -> bool { + if !self.animating.get() || repaint_reason != RepaintReason::NewWebRenderFrame { + return false; + } + + self.waiting_for_frame_timeout.load(Ordering::Relaxed) + } +} + +enum TimerThreadMessage { + Request(TimerEventRequest), + Quit, +} + +/// A thread that manages a [`TimerScheduler`] running in the background of the +/// [`RefreshDriver`]. This is necessary because we need a reliable way of waking up the +/// embedder's main thread, which may just be sleeping until the `EventLoopWaker` asks it +/// to wake up. +/// +/// It would be nice to integrate this somehow into the embedder thread, but it would +/// require both some communication with the embedder and for all embedders to be well +/// behave respecting wakeup timeouts -- a bit too much to ask at the moment. +struct TimerThread { + sender: Sender<TimerThreadMessage>, + join_handle: Option<JoinHandle<()>>, +} + +impl Drop for TimerThread { + fn drop(&mut self) { + let _ = self.sender.send(TimerThreadMessage::Quit); + if let Some(join_handle) = self.join_handle.take() { + let _ = join_handle.join(); + } + } +} + +impl Default for TimerThread { + fn default() -> Self { + let (sender, receiver) = crossbeam_channel::unbounded::<TimerThreadMessage>(); + let join_handle = thread::Builder::new() + .name(String::from("CompositorTimerThread")) + .spawn(move || { + let mut scheduler = TimerScheduler::default(); + + loop { + select! { + recv(receiver) -> message => { + match message { + Ok(TimerThreadMessage::Request(request)) => { + scheduler.schedule_timer(request); + }, + _ => return, + } + }, + recv(scheduler.wait_channel()) -> _message => { + scheduler.dispatch_completed_timers(); + }, + }; + } + }) + .expect("Could not create RefreshDriver timer thread."); + + Self { + sender, + join_handle: Some(join_handle), + } + } +} + +impl TimerThread { + fn queue_timer(&self, duration: Duration, callback: BoxedTimerCallback) { + let _ = self + .sender + .send(TimerThreadMessage::Request(TimerEventRequest { + callback, + source: TimerSource::FromWorker, + id: TimerEventId(0), + duration, + })); + } +} diff --git a/components/compositing/tracing.rs b/components/compositing/tracing.rs index a8bb8b42bb8..65f9bd76c08 100644 --- a/components/compositing/tracing.rs +++ b/components/compositing/tracing.rs @@ -59,6 +59,7 @@ mod from_constellation { Self::GetScreenSize(..) => target!("GetScreenSize"), Self::GetAvailableScreenSize(..) => target!("GetAvailableScreenSize"), Self::CollectMemoryReport(..) => target!("CollectMemoryReport"), + Self::Viewport(..) => target!("Viewport"), } } } diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index b0e91ccb02e..2f822a37e11 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -2,12 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::RefCell; -use std::collections::HashMap; +use std::cell::{Cell, RefCell}; use std::collections::hash_map::Keys; +use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use base::id::{PipelineId, WebViewId}; +use compositing_traits::viewport_description::{ + DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM, ViewportDescription, +}; use compositing_traits::{SendableFrameTree, WebViewTrait}; use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType}; use embedder_traits::{ @@ -25,13 +28,9 @@ use webrender_api::units::{ }; use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation}; -use crate::compositor::{PipelineDetails, ServoRenderer}; +use crate::compositor::{HitTestError, PipelineDetails, ServoRenderer}; use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; -// Default viewport constraints -const MAX_ZOOM: f32 = 8.0; -const MIN_ZOOM: f32 = 0.1; - #[derive(Clone, Copy)] struct ScrollEvent { /// Scroll by this offset, or to Start or End @@ -44,8 +43,10 @@ struct ScrollEvent { #[derive(Clone, Copy)] enum ScrollZoomEvent { - /// An pinch zoom event that magnifies the view by the given factor. + /// A pinch zoom event that magnifies the view by the given factor. PinchZoom(f32), + /// A zoom event that magnifies the view by the factor parsed from meta tag. + ViewportZoom(f32), /// A scroll event that scrolls the scroll node at the given location by the /// given amount. Scroll(ScrollEvent), @@ -58,7 +59,7 @@ pub(crate) struct ScrollResult { pub offset: LayoutVector2D, } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PinchZoomResult { DidPinchZoom, DidNotPinchZoom, @@ -89,15 +90,18 @@ pub(crate) struct WebViewRenderer { pub page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>, /// "Mobile-style" zoom that does not reflow the page. viewport_zoom: PinchZoomFactor, - /// Viewport zoom constraints provided by @viewport. - min_viewport_zoom: Option<PinchZoomFactor>, - max_viewport_zoom: Option<PinchZoomFactor>, /// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled /// by the embedding layer. hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>, /// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with /// active animations or animation frame callbacks. animating: bool, + /// Pending input events queue. Priavte and only this thread pushes events to it. + pending_point_input_events: RefCell<VecDeque<InputEvent>>, + /// WebRender is not ready between `SendDisplayList` and `WebRenderFrameReady` messages. + pub webrender_frame_ready: Cell<bool>, + /// Viewport Description + viewport_description: Option<ViewportDescription>, } impl Drop for WebViewRenderer { @@ -127,11 +131,12 @@ impl WebViewRenderer { global, pending_scroll_zoom_events: Default::default(), page_zoom: Scale::new(1.0), - viewport_zoom: PinchZoomFactor::new(1.0), - min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), - max_viewport_zoom: None, + viewport_zoom: PinchZoomFactor::new(DEFAULT_ZOOM), hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), animating: false, + pending_point_input_events: Default::default(), + webrender_frame_ready: Cell::default(), + viewport_description: None, } } @@ -309,29 +314,89 @@ impl WebViewRenderer { } } - pub(crate) fn dispatch_input_event(&mut self, event: InputEvent) { + pub(crate) fn dispatch_point_input_event(&mut self, mut event: InputEvent) -> bool { // Events that do not need to do hit testing are sent directly to the // constellation to filter down. let Some(point) = event.point() else { - return; + return false; }; + // Delay the event if the epoch is not synchronized yet (new frame is not ready), + // or hit test result would fail and the event is rejected anyway. + if !self.webrender_frame_ready.get() || !self.pending_point_input_events.borrow().is_empty() + { + self.pending_point_input_events + .borrow_mut() + .push_back(event); + return false; + } + // If we can't find a pipeline to send this event to, we cannot continue. let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); - let Some(result) = self + let result = match self .global .borrow() .hit_test_at_point(point, get_pipeline_details) - else { - return; + { + Ok(hit_test_results) => hit_test_results, + Err(HitTestError::EpochMismatch) => { + self.pending_point_input_events + .borrow_mut() + .push_back(event); + return false; + }, + _ => { + return false; + }, }; - self.global.borrow_mut().update_cursor(point, &result); + match event { + InputEvent::Touch(ref mut touch_event) => { + touch_event.init_sequence_id(self.touch_handler.current_sequence_id); + }, + InputEvent::MouseButton(_) | InputEvent::MouseMove(_) | InputEvent::Wheel(_) => { + self.global.borrow_mut().update_cursor(point, &result); + }, + _ => unreachable!("Unexpected input event type: {event:?}"), + } if let Err(error) = self.global.borrow().constellation_sender.send( EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), ) { warn!("Sending event to constellation failed ({error:?})."); + false + } else { + true + } + } + + pub(crate) fn dispatch_pending_point_input_events(&self) { + while let Some(event) = self.pending_point_input_events.borrow_mut().pop_front() { + // Events that do not need to do hit testing are sent directly to the + // constellation to filter down. + let Some(point) = event.point() else { + continue; + }; + + // If we can't find a pipeline to send this event to, we cannot continue. + let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); + let Ok(result) = self + .global + .borrow() + .hit_test_at_point(point, get_pipeline_details) + else { + // Don't need to process pending input events in this frame any more. + // TODO: Add multiple retry later if needed. + return; + }; + + self.global.borrow_mut().update_cursor(point, &result); + + if let Err(error) = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), + ) { + warn!("Sending event to constellation failed ({error:?})."); + } } } @@ -401,29 +466,11 @@ impl WebViewRenderer { } } - self.dispatch_input_event(event); + self.dispatch_point_input_event(event); } - fn send_touch_event(&self, mut event: TouchEvent) -> bool { - let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); - let Some(result) = self - .global - .borrow() - .hit_test_at_point(event.point, get_pipeline_details) - else { - return false; - }; - - event.init_sequence_id(self.touch_handler.current_sequence_id); - let event = InputEvent::Touch(event); - if let Err(e) = self.global.borrow().constellation_sender.send( - EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), - ) { - warn!("Sending event to constellation failed ({:?}).", e); - false - } else { - true - } + fn send_touch_event(&mut self, event: TouchEvent) -> bool { + self.dispatch_point_input_event(InputEvent::Touch(event)) } pub(crate) fn on_touch_event(&mut self, event: TouchEvent) { @@ -687,13 +734,13 @@ impl WebViewRenderer { /// <http://w3c.github.io/touch-events/#mouse-events> fn simulate_mouse_click(&mut self, point: DevicePoint) { let button = MouseButton::Left; - self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point))); - self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + self.dispatch_point_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point))); + self.dispatch_point_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Down, button, point, ))); - self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + self.dispatch_point_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Up, button, point, @@ -764,7 +811,8 @@ impl WebViewRenderer { let mut combined_magnification = 1.0; for scroll_event in self.pending_scroll_zoom_events.drain(..) { match scroll_event { - ScrollZoomEvent::PinchZoom(magnification) => { + ScrollZoomEvent::PinchZoom(magnification) | + ScrollZoomEvent::ViewportZoom(magnification) => { combined_magnification *= magnification }, ScrollZoomEvent::Scroll(scroll_event_info) => { @@ -858,7 +906,8 @@ impl WebViewRenderer { HitTestFlags::FIND_ALL, None, get_pipeline_details, - ); + ) + .unwrap_or_default(); // Iterate through all hit test results, processing only the first node of each pipeline. // This is needed to propagate the scroll events from a pipeline representing an iframe to @@ -892,11 +941,8 @@ impl WebViewRenderer { } fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool { - if let Some(min) = self.min_viewport_zoom { - zoom = f32::max(min.get(), zoom); - } - if let Some(max) = self.max_viewport_zoom { - zoom = f32::min(max.get(), zoom); + if let Some(viewport) = self.viewport_description.as_ref() { + zoom = viewport.clamp_zoom(zoom); } let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom)); @@ -926,7 +972,12 @@ impl WebViewRenderer { // TODO: Scroll to keep the center in view? self.pending_scroll_zoom_events - .push(ScrollZoomEvent::PinchZoom(magnification)); + .push(ScrollZoomEvent::PinchZoom( + self.viewport_description + .clone() + .unwrap_or_default() + .clamp_zoom(magnification), + )); } fn send_window_size_message(&self) { @@ -993,6 +1044,16 @@ impl WebViewRenderer { let screen_geometry = self.webview.screen_geometry().unwrap_or_default(); (screen_geometry.available_size.to_f32() / self.hidpi_scale_factor).to_i32() } + + pub fn set_viewport_description(&mut self, viewport_description: ViewportDescription) { + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::ViewportZoom( + viewport_description + .clone() + .clamp_zoom(viewport_description.initial_scale.get()), + )); + self.viewport_description = Some(viewport_description); + } } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 65d94641322..1d0d8150f00 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -148,6 +148,7 @@ use net_traits::pub_domains::reg_host; use net_traits::request::Referrer; use net_traits::storage_thread::{StorageThreadMsg, StorageType}; use net_traits::{self, IpcSend, ReferrerPolicy, ResourceThreads}; +use profile_traits::mem::ProfilerMsg; use profile_traits::{mem, time}; use script_layout_interface::{LayoutFactory, ScriptThreadFactory}; use script_traits::{ @@ -1488,6 +1489,9 @@ where ) => { self.handle_evaluate_javascript(webview_id, evaluation_id, script); }, + EmbedderToConstellationMessage::CreateMemoryReport(sender) => { + self.mem_profiler_chan.send(ProfilerMsg::Report(sender)); + }, } } @@ -2758,7 +2762,11 @@ where } debug!("Exiting Canvas Paint thread."); - if let Err(e) = self.canvas_sender.send(ConstellationCanvasMsg::Exit) { + let (canvas_exit_sender, canvas_exit_receiver) = unbounded(); + if let Err(e) = self + .canvas_sender + .send(ConstellationCanvasMsg::Exit(canvas_exit_sender)) + { warn!("Exit Canvas Paint thread failed ({})", e); } @@ -2800,6 +2808,10 @@ where debug!("Exiting GLPlayer thread."); WindowGLContext::get().exit(); + // Wait for the canvas thread to exit before shutting down the font service, as + // canvas might still be using the system font service before shutting down. + let _ = canvas_exit_receiver.recv(); + debug!("Exiting the system font service thread."); self.system_font_service.exit(); @@ -4690,29 +4702,50 @@ where WebDriverCommandMsg::CloseWebView(webview_id) => { self.handle_close_top_level_browsing_context(webview_id); }, - WebDriverCommandMsg::NewWebView(webview_id, sender, load_sender) => { - let (chan, port) = match ipc::channel() { + WebDriverCommandMsg::NewWebView( + originating_webview_id, + response_sender, + load_status_sender, + ) => { + let (embedder_sender, receiver) = match ipc::channel() { Ok(result) => result, Err(error) => return warn!("Failed to create channel: {error:?}"), }; - self.embedder_proxy - .send(EmbedderMsg::AllowOpeningWebView(webview_id, chan)); - let (webview_id, viewport_details) = match port.recv() { - Ok(Some((webview_id, viewport_details))) => (webview_id, viewport_details), + self.embedder_proxy.send(EmbedderMsg::AllowOpeningWebView( + originating_webview_id, + embedder_sender, + )); + let (new_webview_id, viewport_details) = match receiver.recv() { + Ok(Some((new_webview_id, viewport_details))) => { + (new_webview_id, viewport_details) + }, Ok(None) => return warn!("Embedder refused to allow opening webview"), Err(error) => return warn!("Failed to receive webview id: {error:?}"), }; self.handle_new_top_level_browsing_context( ServoUrl::parse_with_base(None, "about:blank").expect("Infallible parse"), - webview_id, + new_webview_id, viewport_details, - Some(load_sender), + Some(load_status_sender), ); - let _ = sender.send(webview_id); + if let Err(error) = response_sender.send(new_webview_id) { + error!( + "WebDriverCommandMsg::NewWebView: IPC error when sending new_webview_id \ + to webdriver server: {error}" + ); + } }, WebDriverCommandMsg::FocusWebView(webview_id) => { self.handle_focus_web_view(webview_id); }, + WebDriverCommandMsg::IsWebViewOpen(webview_id, response_sender) => { + let is_open = self.webviews.get(webview_id).is_some(); + let _ = response_sender.send(is_open); + }, + WebDriverCommandMsg::IsBrowsingContextOpen(browsing_context_id, response_sender) => { + let is_open = self.browsing_contexts.contains_key(&browsing_context_id); + let _ = response_sender.send(is_open); + }, WebDriverCommandMsg::GetWindowSize(webview_id, response_sender) => { let browsing_context_id = BrowsingContextId::from(webview_id); let size = self @@ -4866,10 +4899,20 @@ where webview_id, x, y, msg_id, )); }, - WebDriverCommandMsg::WheelScrollAction(webview, x, y, delta_x, delta_y) => { + WebDriverCommandMsg::WheelScrollAction( + webview_id, + x, + y, + delta_x, + delta_y, + msg_id, + response_sender, + ) => { + self.webdriver.input_command_response_sender = Some(response_sender); + self.compositor_proxy .send(CompositorMsg::WebDriverWheelScrollEvent( - webview, x, y, delta_x, delta_y, + webview_id, x, y, delta_x, delta_y, msg_id, )); }, WebDriverCommandMsg::TakeScreenshot(webview_id, rect, response_sender) => { diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 6237665b87f..99551bd58fc 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -78,6 +78,7 @@ mod from_compositor { Self::SetScrollStates(..) => target!("SetScrollStates"), Self::PaintMetric(..) => target!("PaintMetric"), Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"), + Self::CreateMemoryReport(..) => target!("CreateMemoryReport"), } } } diff --git a/components/devtools/actors/breakpoint.rs b/components/devtools/actors/breakpoint.rs new file mode 100644 index 00000000000..04f2de140b4 --- /dev/null +++ b/components/devtools/actors/breakpoint.rs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use serde::Serialize; + +use crate::EmptyReplyMsg; +use crate::actor::{Actor, ActorMessageStatus}; +use crate::protocol::JsonPacketStream; + +#[derive(Serialize)] +pub struct BreakpointListActorMsg { + actor: String, +} + +pub struct BreakpointListActor { + name: String, +} + +impl Actor for BreakpointListActor { + fn name(&self) -> String { + self.name.clone() + } + + fn handle_message( + &self, + _registry: &crate::actor::ActorRegistry, + msg_type: &str, + _msg: &serde_json::Map<String, serde_json::Value>, + stream: &mut std::net::TcpStream, + _stream_id: crate::StreamId, + ) -> Result<crate::actor::ActorMessageStatus, ()> { + Ok(match msg_type { + "setBreakpoint" => { + let msg = EmptyReplyMsg { from: self.name() }; + let _ = stream.write_json_packet(&msg); + + ActorMessageStatus::Processed + }, + "setActiveEventBreakpoints" => { + let msg = EmptyReplyMsg { from: self.name() }; + let _ = stream.write_json_packet(&msg); + + ActorMessageStatus::Processed + }, + _ => ActorMessageStatus::Ignored, + }) + } +} + +impl BreakpointListActor { + pub fn new(name: String) -> Self { + Self { name } + } +} diff --git a/components/devtools/actors/browsing_context.rs b/components/devtools/actors/browsing_context.rs index 81a00e82d47..fc5116131a0 100644 --- a/components/devtools/actors/browsing_context.rs +++ b/components/devtools/actors/browsing_context.rs @@ -82,6 +82,13 @@ struct BrowsingContextTraits { } #[derive(Serialize)] +#[serde(rename_all = "lowercase")] +enum TargetType { + Frame, + // Other target types not implemented yet. +} + +#[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct BrowsingContextActorMsg { actor: String, @@ -104,6 +111,7 @@ pub struct BrowsingContextActorMsg { reflow_actor: String, style_sheets_actor: String, thread_actor: String, + target_type: TargetType, // Part of the official protocol, but not yet implemented. // animations_actor: String, // changes_actor: String, @@ -302,6 +310,7 @@ impl BrowsingContextActor { reflow_actor: self.reflow.clone(), style_sheets_actor: self.style_sheets.clone(), thread_actor: self.thread.clone(), + target_type: TargetType::Frame, } } diff --git a/components/devtools/actors/network_event.rs b/components/devtools/actors/network_event.rs index fdfc5d7b581..f95ee733045 100644 --- a/components/devtools/actors/network_event.rs +++ b/components/devtools/actors/network_event.rs @@ -18,6 +18,7 @@ use serde_json::{Map, Value}; use crate::StreamId; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; +use crate::network_handler::Cause; use crate::protocol::JsonPacketStream; struct HttpRequest { @@ -44,7 +45,7 @@ pub struct NetworkEventActor { is_xhr: bool, } -#[derive(Serialize)] +#[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct EventActor { pub actor: String, @@ -55,6 +56,7 @@ pub struct EventActor { #[serde(rename = "isXHR")] pub is_xhr: bool, pub private: bool, + pub cause: Cause, } #[derive(Serialize)] @@ -392,6 +394,14 @@ impl NetworkEventActor { LocalResult::Ambiguous(date_time, _) => date_time.to_rfc3339().to_string(), }; + let cause_type = match self.request.url.as_str() { + // Adjust based on request data + url if url.ends_with(".css") => "stylesheet", + url if url.ends_with(".js") => "script", + url if url.ends_with(".png") || url.ends_with(".jpg") => "img", + _ => "document", + }; + EventActor { actor: self.name(), url: self.request.url.clone(), @@ -400,6 +410,10 @@ impl NetworkEventActor { time_stamp: self.request.time_stamp, is_xhr: self.is_xhr, private: false, + cause: Cause { + type_: cause_type.to_string(), + loading_document_uri: None, // Set if available + }, } } diff --git a/components/devtools/actors/root.rs b/components/devtools/actors/root.rs index 8ad21fc4bda..01ec4caedb1 100644 --- a/components/devtools/actors/root.rs +++ b/components/devtools/actors/root.rs @@ -295,7 +295,7 @@ impl RootActor { sources: false, highlightable: true, custom_highlighters: true, - network_monitor: false, + network_monitor: true, }, } } diff --git a/components/devtools/actors/watcher.rs b/components/devtools/actors/watcher.rs index 061ffc92336..7720daf070d 100644 --- a/components/devtools/actors/watcher.rs +++ b/components/devtools/actors/watcher.rs @@ -19,6 +19,7 @@ use serde::Serialize; use serde_json::{Map, Value}; use self::network_parent::{NetworkParentActor, NetworkParentActorMsg}; +use super::breakpoint::BreakpointListActor; use super::thread::ThreadActor; use super::worker::WorkerMsg; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; @@ -362,10 +363,14 @@ impl Actor for WatcherActor { ActorMessageStatus::Processed }, "getBreakpointListActor" => { + let breakpoint_list_name = registry.new_name("breakpoint-list"); + let breakpoint_list = BreakpointListActor::new(breakpoint_list_name.clone()); + registry.register_later(Box::new(breakpoint_list)); + let _ = stream.write_json_packet(&GetBreakpointListActorReply { from: self.name(), breakpoint_list: GetBreakpointListActorReplyInner { - actor: registry.new_name("breakpoint-list"), + actor: breakpoint_list_name, }, }); ActorMessageStatus::Processed diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index d097cb25e9d..a623a892323 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -53,6 +53,7 @@ use crate::protocol::JsonPacketStream; mod actor; /// <https://searchfox.org/mozilla-central/source/devtools/server/actors> mod actors { + pub mod breakpoint; pub mod browsing_context; pub mod console; pub mod device; @@ -477,18 +478,21 @@ impl DevtoolsInstance { request_id: String, network_event: NetworkEvent, ) { - let console_actor_name = match self.find_console_actor(pipeline_id, None) { - Some(name) => name, - None => return, - }; let netevent_actor_name = self.find_network_event_actor(request_id); + let Some(id) = self.pipelines.get(&pipeline_id) else { + return; + }; + let Some(browsing_context_actor_name) = self.browsing_contexts.get(id) else { + return; + }; + handle_network_event( Arc::clone(&self.actors), - console_actor_name, netevent_actor_name, connections, network_event, + browsing_context_actor_name.to_string(), ) } diff --git a/components/devtools/network_handler.rs b/components/devtools/network_handler.rs index a6d21820c2c..f210c3ee170 100644 --- a/components/devtools/network_handler.rs +++ b/components/devtools/network_handler.rs @@ -9,17 +9,10 @@ use devtools_traits::NetworkEvent; use serde::Serialize; use crate::actor::ActorRegistry; -use crate::actors::network_event::{EventActor, NetworkEventActor, ResponseStartMsg}; +use crate::actors::browsing_context::BrowsingContextActor; +use crate::actors::network_event::{NetworkEventActor, ResponseStartMsg}; use crate::protocol::JsonPacketStream; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct NetworkEventMsg { - from: String, - #[serde(rename = "type")] - type_: String, - event_actor: EventActor, -} +use crate::resource::ResourceAvailable; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -50,34 +43,44 @@ struct EventTimingsUpdateMsg { struct SecurityInfoUpdateMsg { state: String, } +#[derive(Clone, Serialize)] +pub struct Cause { + #[serde(rename = "type")] + pub type_: String, + #[serde(rename = "loadingDocumentUri")] + pub loading_document_uri: Option<String>, +} pub(crate) fn handle_network_event( actors: Arc<Mutex<ActorRegistry>>, - console_actor_name: String, netevent_actor_name: String, mut connections: Vec<TcpStream>, network_event: NetworkEvent, + browsing_context_actor_name: String, ) { let mut actors = actors.lock().unwrap(); - let actor = actors.find_mut::<NetworkEventActor>(&netevent_actor_name); - match network_event { NetworkEvent::HttpRequest(httprequest) => { - // Store the request information in the actor - actor.add_request(httprequest); - - // Send a networkEvent message to the client - let msg = NetworkEventMsg { - from: console_actor_name, - type_: "networkEvent".to_owned(), - event_actor: actor.event_actor(), + // Scope mutable borrow + let event_actor = { + let actor = actors.find_mut::<NetworkEventActor>(&netevent_actor_name); + actor.add_request(httprequest); + actor.event_actor() }; + + let browsing_context_actor = + actors.find::<BrowsingContextActor>(&browsing_context_actor_name); for stream in &mut connections { - let _ = stream.write_json_packet(&msg); + browsing_context_actor.resource_available( + event_actor.clone(), + "network-event".to_string(), + stream, + ); } }, NetworkEvent::HttpResponse(httpresponse) => { // Store the response information in the actor + let actor = actors.find_mut::<NetworkEventActor>(&netevent_actor_name); actor.add_response(httpresponse); let msg = NetworkEventUpdateMsg { diff --git a/components/layout/display_list/stacking_context.rs b/components/layout/display_list/stacking_context.rs index 66d8421e5f7..17e8f2ef4fd 100644 --- a/components/layout/display_list/stacking_context.rs +++ b/components/layout/display_list/stacking_context.rs @@ -618,7 +618,7 @@ impl StackingContext { // If it’s larger, we also want to paint areas reachable after scrolling. let painting_area = fragment_tree .initial_containing_block - .union(&fragment_tree.scrollable_overflow) + .union(&fragment_tree.scrollable_overflow()) .to_webrender(); let background_color = @@ -1346,7 +1346,7 @@ impl BoxFragment { let position = self.style.get_box().position; // https://drafts.csswg.org/css2/#clipping // The clip property applies only to absolutely positioned elements - if position != ComputedPosition::Absolute && position != ComputedPosition::Fixed { + if !position.is_absolutely_positioned() { return None; } diff --git a/components/layout/flow/construct.rs b/components/layout/flow/construct.rs index cc3fe0e6f44..d8d6cd5c6ab 100644 --- a/components/layout/flow/construct.rs +++ b/components/layout/flow/construct.rs @@ -744,9 +744,14 @@ impl BlockLevelJob<'_> { self.propagated_data, false, /* is_list_item */ ); + // An outside ::marker must establish a BFC, and can't contain floats. + let block_formatting_context = BlockFormattingContext { + contents: block_container, + contains_floats: false, + }; ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker { base: LayoutBoxBase::new(info.into(), info.style.clone()), - block_container, + block_formatting_context, list_item_style, })) }, diff --git a/components/layout/flow/mod.rs b/components/layout/flow/mod.rs index 4776b65771c..92cabc4b904 100644 --- a/components/layout/flow/mod.rs +++ b/components/layout/flow/mod.rs @@ -308,7 +308,7 @@ pub(crate) struct CollapsibleWithParentStartMargin(bool); pub(crate) struct OutsideMarker { pub list_item_style: Arc<ComputedValues>, pub base: LayoutBoxBase, - pub block_container: BlockContainer, + pub block_formatting_context: BlockFormattingContext, } impl OutsideMarker { @@ -317,8 +317,11 @@ impl OutsideMarker { layout_context: &LayoutContext, constraint_space: &ConstraintSpace, ) -> InlineContentSizesResult { - self.base - .inline_content_sizes(layout_context, constraint_space, &self.block_container) + self.base.inline_content_sizes( + layout_context, + constraint_space, + &self.block_formatting_context.contents, + ) } fn layout( @@ -326,8 +329,6 @@ impl OutsideMarker { layout_context: &LayoutContext<'_>, containing_block: &ContainingBlock<'_>, positioning_context: &mut PositioningContext, - sequential_layout_state: Option<&mut SequentialLayoutState>, - collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>, ) -> Fragment { let constraint_space = ConstraintSpace::new_for_style_and_ratio( &self.base.style, @@ -342,17 +343,11 @@ impl OutsideMarker { style: &self.base.style, }; - // A ::marker can't have a stretch size (must be auto), so this doesn't matter. - // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing - let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false); - - let flow_layout = self.block_container.layout( + let flow_layout = self.block_formatting_context.layout( layout_context, positioning_context, &containing_block_for_children, - sequential_layout_state, - collapsible_with_parent_start_margin.unwrap_or(CollapsibleWithParentStartMargin(false)), - ignore_block_margins_for_stretch, + false, /* depends_on_block_constraints */ ); let max_inline_size = @@ -900,13 +895,9 @@ impl BlockLevelBox { BlockLevelBox::OutOfFlowFloatBox(float_box) => Fragment::Float(ArcRefCell::new( float_box.layout(layout_context, positioning_context, containing_block), )), - BlockLevelBox::OutsideMarker(outside_marker) => outside_marker.layout( - layout_context, - containing_block, - positioning_context, - sequential_layout_state, - collapsible_with_parent_start_margin, - ), + BlockLevelBox::OutsideMarker(outside_marker) => { + outside_marker.layout(layout_context, containing_block, positioning_context) + }, }; self.with_base(|base| base.set_fragment(fragment.clone())); @@ -2032,8 +2023,10 @@ fn solve_inline_margins_avoiding_floats( placement_rect: LogicalRect<Au>, justify_self: AlignFlags, ) -> ((Au, Au), Au) { - let free_space = placement_rect.size.inline - inline_size; - debug_assert!(free_space >= Au::zero()); + // PlacementAmongFloats should guarantee that the inline size of the placement rect + // is at least as big as `inline_size`. However, that may fail when dealing with + // huge sizes that need to be saturated to MAX_AU, so floor by zero. See #37312. + let free_space = Au::zero().max(placement_rect.size.inline - inline_size); let cb_info = &sequential_layout_state.floats.containing_block_info; let start_adjustment = placement_rect.start_corner.inline - cb_info.inline_start; let end_adjustment = cb_info.inline_end - placement_rect.max_inline_position(); diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index fb9884a4f01..fe98dbc3156 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -29,7 +29,7 @@ use crate::flow::inline::InlineItem; use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; use crate::formatting_contexts::IndependentFormattingContext; use crate::fragment_tree::FragmentTree; -use crate::geom::{LogicalVec2, PhysicalRect, PhysicalSize}; +use crate::geom::{LogicalVec2, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::replaced::ReplacedContents; use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside}; @@ -392,31 +392,9 @@ impl BoxTree { &mut root_fragments, ); - let scrollable_overflow = root_fragments - .iter() - .fold(PhysicalRect::zero(), |acc, child| { - let child_overflow = child.scrollable_overflow_for_parent(); - - // https://drafts.csswg.org/css-overflow/#scrolling-direction - // We want to clip scrollable overflow on box-start and inline-start - // sides of the scroll container. - // - // FIXME(mrobinson, bug 25564): This should take into account writing - // mode. - let child_overflow = PhysicalRect::new( - euclid::Point2D::zero(), - euclid::Size2D::new( - child_overflow.size.width + child_overflow.origin.x, - child_overflow.size.height + child_overflow.origin.y, - ), - ); - acc.union(&child_overflow) - }); - FragmentTree::new( layout_context, root_fragments, - scrollable_overflow, physical_containing_block, self.viewport_scroll_sensitivity, ) diff --git a/components/layout/fragment_tree/box_fragment.rs b/components/layout/fragment_tree/box_fragment.rs index b7c3a2a3524..eb63038b7d7 100644 --- a/components/layout/fragment_tree/box_fragment.rs +++ b/components/layout/fragment_tree/box_fragment.rs @@ -89,7 +89,7 @@ pub(crate) struct BoxFragment { block_margins_collapsed_with_children: Option<Box<CollapsedBlockMargins>>, /// The scrollable overflow of this box fragment. - pub scrollable_overflow_from_children: PhysicalRect<Au>, + scrollable_overflow: Option<PhysicalRect<Au>>, /// The resolved box insets if this box is `position: sticky`. These are calculated /// during `StackingContextTree` construction because they rely on the size of the @@ -114,11 +114,6 @@ impl BoxFragment { margin: PhysicalSides<Au>, clearance: Option<Au>, ) -> BoxFragment { - let scrollable_overflow_from_children = - children.iter().fold(PhysicalRect::zero(), |acc, child| { - acc.union(&child.scrollable_overflow_for_parent()) - }); - BoxFragment { base: base_fragment_info.into(), style, @@ -131,7 +126,7 @@ impl BoxFragment { clearance, baselines: Baselines::default(), block_margins_collapsed_with_children: None, - scrollable_overflow_from_children, + scrollable_overflow: None, resolved_sticky_insets: AtomicRefCell::default(), background_mode: BackgroundMode::Normal, specific_layout_info: None, @@ -203,13 +198,23 @@ impl BoxFragment { /// Get the scrollable overflow for this [`BoxFragment`] relative to its /// containing block. pub fn scrollable_overflow(&self) -> PhysicalRect<Au> { + self.scrollable_overflow + .expect("Should only call `scrollable_overflow()` after calculating overflow") + } + + pub(crate) fn calculate_scrollable_overflow(&mut self) { + let scrollable_overflow_from_children = self + .children + .iter() + .fold(PhysicalRect::zero(), |acc, child| { + acc.union(&child.calculate_scrollable_overflow_for_parent()) + }); let physical_padding_rect = self.padding_rect(); let content_origin = self.content_rect.origin.to_vector(); - physical_padding_rect.union( - &self - .scrollable_overflow_from_children - .translate(content_origin), - ) + self.scrollable_overflow = Some( + physical_padding_rect + .union(&scrollable_overflow_from_children.translate(content_origin)), + ); } pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) { @@ -275,7 +280,12 @@ impl BoxFragment { tree.end_level(); } - pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { + pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { + // TODO: Properly handle absolutely positioned fragments. + if self.style.get_box().position.is_absolutely_positioned() { + return PhysicalRect::zero(); + } + let mut overflow = self.border_rect(); if !self.style.establishes_scroll_container(self.base.flags) { // https://www.w3.org/TR/css-overflow-3/#scrollable @@ -328,7 +338,7 @@ impl BoxFragment { /// /// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction. /// For an element, the clip rect is the padding rect and for viewport, it is the initial containing block. - pub fn clip_unreachable_scrollable_overflow_region( + pub(crate) fn clip_unreachable_scrollable_overflow_region( &self, scrollable_overflow: PhysicalRect<Au>, clipping_rect: PhysicalRect<Au>, @@ -362,7 +372,7 @@ impl BoxFragment { /// /// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction. /// This will coincides with the scrollport if the fragment is a scroll container. - pub fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> { + pub(crate) fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> { self.clip_unreachable_scrollable_overflow_region( self.scrollable_overflow(), self.padding_rect(), @@ -421,9 +431,7 @@ impl BoxFragment { return convert_to_au_or_auto(PhysicalSides::new(top, right, bottom, left)); } - debug_assert!( - position == ComputedPosition::Fixed || position == ComputedPosition::Absolute - ); + debug_assert!(position.is_absolutely_positioned()); let margin_rect = self.margin_rect(); let (top, bottom) = match (&insets.top, &insets.bottom) { diff --git a/components/layout/fragment_tree/fragment.rs b/components/layout/fragment_tree/fragment.rs index c81fd59e36b..10338c78743 100644 --- a/components/layout/fragment_tree/fragment.rs +++ b/components/layout/fragment_tree/fragment.rs @@ -183,19 +183,36 @@ impl Fragment { } } - pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { + pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { match self { Fragment::Box(fragment) | Fragment::Float(fragment) => { - fragment.borrow().scrollable_overflow_for_parent() + return fragment.borrow().scrollable_overflow_for_parent(); }, Fragment::AbsoluteOrFixedPositioned(_) => PhysicalRect::zero(), - Fragment::Positioning(fragment) => fragment.borrow().scrollable_overflow, + Fragment::Positioning(fragment) => fragment.borrow().scrollable_overflow_for_parent(), Fragment::Text(fragment) => fragment.borrow().rect, Fragment::Image(fragment) => fragment.borrow().rect, Fragment::IFrame(fragment) => fragment.borrow().rect, } } + pub(crate) fn calculate_scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { + self.calculate_scrollable_overflow(); + self.scrollable_overflow_for_parent() + } + + pub(crate) fn calculate_scrollable_overflow(&self) { + match self { + Fragment::Box(fragment) | Fragment::Float(fragment) => { + fragment.borrow_mut().calculate_scrollable_overflow() + }, + Fragment::Positioning(fragment) => { + fragment.borrow_mut().calculate_scrollable_overflow() + }, + _ => {}, + } + } + pub(crate) fn cumulative_border_box_rect(&self) -> Option<PhysicalRect<Au>> { match self { Fragment::Box(fragment) | Fragment::Float(fragment) => { diff --git a/components/layout/fragment_tree/fragment_tree.rs b/components/layout/fragment_tree/fragment_tree.rs index ba03a72ac21..b59ace43aa6 100644 --- a/components/layout/fragment_tree/fragment_tree.rs +++ b/components/layout/fragment_tree/fragment_tree.rs @@ -2,14 +2,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cell::Cell; + use app_units::Au; use base::print_tree::PrintTree; use compositing_traits::display_list::AxesScrollSensitivity; -use euclid::default::Size2D; use fxhash::FxHashSet; use malloc_size_of_derive::MallocSizeOf; use style::animation::AnimationSetKey; -use webrender_api::units; use super::{BoxFragment, ContainingBlockManager, Fragment}; use crate::ArcRefCell; @@ -30,7 +30,7 @@ pub struct FragmentTree { /// The scrollable overflow rectangle for the entire tree /// <https://drafts.csswg.org/css-overflow/#scrollable> - pub(crate) scrollable_overflow: PhysicalRect<Au>, + scrollable_overflow: Cell<Option<PhysicalRect<Au>>>, /// The containing block used in the layout of this fragment tree. pub(crate) initial_containing_block: PhysicalRect<Au>, @@ -43,13 +43,12 @@ impl FragmentTree { pub(crate) fn new( layout_context: &LayoutContext, root_fragments: Vec<Fragment>, - scrollable_overflow: PhysicalRect<Au>, initial_containing_block: PhysicalRect<Au>, viewport_scroll_sensitivity: AxesScrollSensitivity, ) -> Self { let fragment_tree = Self { root_fragments, - scrollable_overflow, + scrollable_overflow: Cell::default(), initial_containing_block, viewport_scroll_sensitivity, }; @@ -97,11 +96,35 @@ impl FragmentTree { } } - pub fn scrollable_overflow(&self) -> units::LayoutSize { - units::LayoutSize::from_untyped(Size2D::new( - self.scrollable_overflow.size.width.to_f32_px(), - self.scrollable_overflow.size.height.to_f32_px(), - )) + pub(crate) fn scrollable_overflow(&self) -> PhysicalRect<Au> { + self.scrollable_overflow + .get() + .expect("Should only call `scrollable_overflow()` after calculating overflow") + } + + pub(crate) fn calculate_scrollable_overflow(&self) { + self.scrollable_overflow + .set(Some(self.root_fragments.iter().fold( + PhysicalRect::zero(), + |acc, child| { + let child_overflow = child.calculate_scrollable_overflow_for_parent(); + + // https://drafts.csswg.org/css-overflow/#scrolling-direction + // We want to clip scrollable overflow on box-start and inline-start + // sides of the scroll container. + // + // FIXME(mrobinson, bug 25564): This should take into account writing + // mode. + let child_overflow = PhysicalRect::new( + euclid::Point2D::zero(), + euclid::Size2D::new( + child_overflow.size.width + child_overflow.origin.x, + child_overflow.size.height + child_overflow.origin.y, + ), + ); + acc.union(&child_overflow) + }, + ))); } pub(crate) fn find<T>( diff --git a/components/layout/fragment_tree/positioning_fragment.rs b/components/layout/fragment_tree/positioning_fragment.rs index e45a6137bff..5547a9d86a1 100644 --- a/components/layout/fragment_tree/positioning_fragment.rs +++ b/components/layout/fragment_tree/positioning_fragment.rs @@ -22,7 +22,7 @@ pub(crate) struct PositioningFragment { pub children: Vec<Fragment>, /// The scrollable overflow of this anonymous fragment's children. - pub scrollable_overflow: PhysicalRect<Au>, + scrollable_overflow: Option<PhysicalRect<Au>>, /// The style of the fragment. pub style: ServoArc<ComputedValues>, @@ -55,20 +55,12 @@ impl PositioningFragment { rect: PhysicalRect<Au>, children: Vec<Fragment>, ) -> ArcRefCell<Self> { - let content_origin = rect.origin; - let scrollable_overflow = children.iter().fold(PhysicalRect::zero(), |acc, child| { - acc.union( - &child - .scrollable_overflow_for_parent() - .translate(content_origin.to_vector()), - ) - }); ArcRefCell::new(PositioningFragment { base, style, rect, children, - scrollable_overflow, + scrollable_overflow: None, cumulative_containing_block_rect: PhysicalRect::zero(), }) } @@ -81,6 +73,25 @@ impl PositioningFragment { rect.translate(self.cumulative_containing_block_rect.origin.to_vector()) } + pub(crate) fn calculate_scrollable_overflow(&mut self) { + self.scrollable_overflow = Some(self.children.iter().fold( + PhysicalRect::zero(), + |acc, child| { + acc.union( + &child + .calculate_scrollable_overflow_for_parent() + .translate(self.rect.origin.to_vector()), + ) + }, + )); + } + + pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { + self.scrollable_overflow.expect( + "Should only call `scrollable_overflow_for_parent()` after calculating overflow", + ) + } + pub fn print(&self, tree: &mut PrintTree) { tree.new_level(format!( "PositioningFragment\ diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 54edc215389..0fbc8395c35 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -8,6 +8,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::fmt::Debug; use std::process; +use std::rc::Rc; use std::sync::{Arc, LazyLock}; use app_units::Au; @@ -142,7 +143,7 @@ pub struct LayoutThread { box_tree: RefCell<Option<Arc<BoxTree>>>, /// The fragment tree. - fragment_tree: RefCell<Option<Arc<FragmentTree>>>, + fragment_tree: RefCell<Option<Rc<FragmentTree>>>, /// The [`StackingContextTree`] cached from previous layouts. stacking_context_tree: RefCell<Option<StackingContextTree>>, @@ -656,7 +657,7 @@ impl LayoutThread { &mut layout_context, viewport_changed, ); - + self.calculate_overflow(damage); self.build_stacking_context_tree(&reflow_request, damage); self.build_display_list(&reflow_request, &mut layout_context); @@ -773,8 +774,11 @@ impl LayoutThread { driver::traverse_dom(&recalc_style_traversal, token, rayon_pool).as_node(); let root_node = root_element.as_node(); - let damage = compute_damage_and_repair_style(layout_context.shared_context(), root_node); - if !viewport_changed && !damage.contains(RestyleDamage::REBUILD_BOX) { + let mut damage = + compute_damage_and_repair_style(layout_context.shared_context(), root_node); + if viewport_changed { + damage = RestyleDamage::REBUILD_BOX; + } else if !damage.contains(RestyleDamage::REBUILD_BOX) { layout_context.style_context.stylist.rule_tree().maybe_gc(); return damage; } @@ -802,15 +806,12 @@ impl LayoutThread { .unwrap() .layout(recalc_style_traversal.context(), viewport_size) }; - let fragment_tree = Arc::new(if let Some(pool) = rayon_pool { + let fragment_tree = Rc::new(if let Some(pool) = rayon_pool { pool.install(run_layout) } else { run_layout() }); - if self.debug.dump_flow_tree { - fragment_tree.print(); - } *self.fragment_tree.borrow_mut() = Some(fragment_tree); // The FragmentTree has been updated, so any existing StackingContext tree that layout @@ -837,6 +838,19 @@ impl LayoutThread { damage } + fn calculate_overflow(&self, damage: RestyleDamage) { + if !damage.contains(RestyleDamage::RECALCULATE_OVERFLOW) { + return; + } + + if let Some(fragment_tree) = &*self.fragment_tree.borrow() { + fragment_tree.calculate_scrollable_overflow(); + if self.debug.dump_flow_tree { + fragment_tree.print(); + } + } + } + fn build_stacking_context_tree(&self, reflow_request: &ReflowRequest, damage: RestyleDamage) { if !reflow_request.reflow_goal.needs_display_list() && !reflow_request.reflow_goal.needs_display() @@ -858,13 +872,19 @@ impl LayoutThread { viewport_size.height.to_f32_px(), ); + let scrollable_overflow = fragment_tree.scrollable_overflow(); + let scrollable_overflow = LayoutSize::from_untyped(Size2D::new( + scrollable_overflow.size.width.to_f32_px(), + scrollable_overflow.size.height.to_f32_px(), + )); + // Build the StackingContextTree. This turns the `FragmentTree` into a // tree of fragments in CSS painting order and also creates all // applicable spatial and clip nodes. *self.stacking_context_tree.borrow_mut() = Some(StackingContextTree::new( fragment_tree, viewport_size, - fragment_tree.scrollable_overflow(), + scrollable_overflow, self.id.into(), fragment_tree.viewport_scroll_sensitivity, self.first_reflow.get(), diff --git a/components/layout/positioned.rs b/components/layout/positioned.rs index 6280864d533..186fc78e3bf 100644 --- a/components/layout/positioned.rs +++ b/components/layout/positioned.rs @@ -305,10 +305,7 @@ impl PositioningContext { } pub(crate) fn push(&mut self, hoisted_box: HoistedAbsolutelyPositionedBox) { - debug_assert!(matches!( - hoisted_box.position(), - Position::Absolute | Position::Fixed - )); + debug_assert!(hoisted_box.position().is_absolutely_positioned()); self.absolutes.push(hoisted_box); } @@ -380,7 +377,7 @@ impl HoistedAbsolutelyPositionedBox { .context .style() .clone_position(); - assert!(position == Position::Fixed || position == Position::Absolute); + assert!(position.is_absolutely_positioned()); position } diff --git a/components/layout/query.rs b/components/layout/query.rs index ca9db9ceaf1..7f304ff2da1 100644 --- a/components/layout/query.rs +++ b/components/layout/query.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Utilities for querying the layout, as needed by layout. -use std::sync::Arc; +use std::rc::Rc; use app_units::Au; use euclid::default::{Point2D, Rect}; @@ -80,7 +80,7 @@ pub fn process_client_rect_request(node: ServoLayoutNode<'_>) -> Rect<i32> { /// <https://drafts.csswg.org/cssom-view/#scrolling-area> pub fn process_node_scroll_area_request( requested_node: Option<ServoLayoutNode<'_>>, - fragment_tree: Option<Arc<FragmentTree>>, + fragment_tree: Option<Rc<FragmentTree>>, ) -> Rect<i32> { let Some(tree) = fragment_tree else { return Rect::zero(); diff --git a/components/layout/stylesheets/servo.css b/components/layout/stylesheets/servo.css index c025b19f364..cb206bbcd00 100644 --- a/components/layout/stylesheets/servo.css +++ b/components/layout/stylesheets/servo.css @@ -265,4 +265,8 @@ select { padding: 0 0.25em; /* Don't show a text cursor when hovering selected option */ cursor: default; +} + +slot { + display: contents; }
\ No newline at end of file diff --git a/components/layout/taffy/layout.rs b/components/layout/taffy/layout.rs index 61c4a0508e9..c5f66eee6d2 100644 --- a/components/layout/taffy/layout.rs +++ b/components/layout/taffy/layout.rs @@ -107,7 +107,7 @@ impl taffy::LayoutPartialTree for TaffyContainerContext<'_> { Self: 'a; fn get_core_container_style(&self, _node_id: taffy::NodeId) -> Self::CoreContainerStyle<'_> { - TaffyStyloStyle(self.style) + TaffyStyloStyle::new(self.style, false /* is_replaced */) } fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &taffy::Layout) { @@ -311,7 +311,7 @@ impl taffy::LayoutGridContainer for TaffyContainerContext<'_> { &self, _node_id: taffy::prelude::NodeId, ) -> Self::GridContainerStyle<'_> { - TaffyStyloStyle(self.style) + TaffyStyloStyle::new(self.style, false /* is_replaced */) } fn get_grid_child_style( @@ -320,7 +320,10 @@ impl taffy::LayoutGridContainer for TaffyContainerContext<'_> { ) -> Self::GridItemStyle<'_> { let id = usize::from(child_node_id); let child = (*self.source_child_nodes[id]).borrow(); - TaffyStyloStyle(AtomicRef::map(child, |c| &*c.style)) + // TODO: account for non-replaced elements that are "compressible replaced" + let is_replaced = child.is_in_flow_replaced(); + let stylo_style = AtomicRef::map(child, |c| &*c.style); + TaffyStyloStyle::new(stylo_style, is_replaced) } fn set_detailed_grid_info( diff --git a/components/layout/taffy/mod.rs b/components/layout/taffy/mod.rs index 05fc09c5511..d34f36f249a 100644 --- a/components/layout/taffy/mod.rs +++ b/components/layout/taffy/mod.rs @@ -19,7 +19,9 @@ use crate::construct_modern::{ModernContainerBuilder, ModernItemKind}; use crate::context::LayoutContext; use crate::dom::LayoutBox; use crate::dom_traversal::{NodeAndStyleInfo, NonReplacedContents}; -use crate::formatting_contexts::IndependentFormattingContext; +use crate::formatting_contexts::{ + IndependentFormattingContext, IndependentFormattingContextContents, +}; use crate::fragment_tree::Fragment; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; @@ -166,6 +168,16 @@ impl TaffyItemBox { .repair_style(context, node, new_style), } } + + fn is_in_flow_replaced(&self) -> bool { + match &self.taffy_level_box { + TaffyItemBoxInner::InFlowBox(fc) => match fc.contents { + IndependentFormattingContextContents::NonReplaced(_) => false, + IndependentFormattingContextContents::Replaced(_) => true, + }, + TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_) => false, + } + } } /// Details from Taffy grid layout that will be stored diff --git a/components/layout/taffy/stylo_taffy/convert.rs b/components/layout/taffy/stylo_taffy/convert.rs index 5780be17c82..1b365cde6f6 100644 --- a/components/layout/taffy/stylo_taffy/convert.rs +++ b/components/layout/taffy/stylo_taffy/convert.rs @@ -7,6 +7,7 @@ mod stylo { pub(crate) use style::properties::generated::longhands::box_sizing::computed_value::T as BoxSizing; pub(crate) use style::properties::longhands::aspect_ratio::computed_value::T as AspectRatio; pub(crate) use style::properties::longhands::position::computed_value::T as Position; + pub(crate) use style::values::computed::length_percentage::Unpacked as UnpackedLengthPercentage; pub(crate) use style::values::computed::{LengthPercentage, Percentage}; pub(crate) use style::values::generics::NonNegative; pub(crate) use style::values::generics::length::{ @@ -32,15 +33,16 @@ mod stylo { pub(crate) use style::values::specified::GenericGridTemplateComponent; } +use taffy::MaxTrackSizingFunction; +use taffy::style_helpers::*; + #[inline] pub fn length_percentage(val: &stylo::LengthPercentage) -> taffy::LengthPercentage { - if let Some(length) = val.to_length() { - taffy::LengthPercentage::Length(length.px()) - } else if let Some(val) = val.to_percentage() { - taffy::LengthPercentage::Percent(val.0) - } else { + match val.unpack() { + stylo::UnpackedLengthPercentage::Length(len) => length(len.px()), + stylo::UnpackedLengthPercentage::Percentage(percentage) => percent(percentage.0), // TODO: Support calc - taffy::LengthPercentage::Percent(0.0) + stylo::UnpackedLengthPercentage::Calc(_) => percent(0.0), } } @@ -48,14 +50,14 @@ pub fn length_percentage(val: &stylo::LengthPercentage) -> taffy::LengthPercenta pub fn dimension(val: &stylo::Size) -> taffy::Dimension { match val { stylo::Size::LengthPercentage(val) => length_percentage(&val.0).into(), - stylo::Size::Auto => taffy::Dimension::Auto, + stylo::Size::Auto => taffy::Dimension::AUTO, // TODO: implement other values in Taffy - stylo::Size::MaxContent => taffy::Dimension::Auto, - stylo::Size::MinContent => taffy::Dimension::Auto, - stylo::Size::FitContent => taffy::Dimension::Auto, - stylo::Size::FitContentFunction(_) => taffy::Dimension::Auto, - stylo::Size::Stretch => taffy::Dimension::Auto, + stylo::Size::MaxContent => taffy::Dimension::AUTO, + stylo::Size::MinContent => taffy::Dimension::AUTO, + stylo::Size::FitContent => taffy::Dimension::AUTO, + stylo::Size::FitContentFunction(_) => taffy::Dimension::AUTO, + stylo::Size::Stretch => taffy::Dimension::AUTO, // Anchor positioning will be flagged off for time being stylo::Size::AnchorSizeFunction(_) => unreachable!(), @@ -67,14 +69,14 @@ pub fn dimension(val: &stylo::Size) -> taffy::Dimension { pub fn max_size_dimension(val: &stylo::MaxSize) -> taffy::Dimension { match val { stylo::MaxSize::LengthPercentage(val) => length_percentage(&val.0).into(), - stylo::MaxSize::None => taffy::Dimension::Auto, + stylo::MaxSize::None => taffy::Dimension::AUTO, // TODO: implement other values in Taffy - stylo::MaxSize::MaxContent => taffy::Dimension::Auto, - stylo::MaxSize::MinContent => taffy::Dimension::Auto, - stylo::MaxSize::FitContent => taffy::Dimension::Auto, - stylo::MaxSize::FitContentFunction(_) => taffy::Dimension::Auto, - stylo::MaxSize::Stretch => taffy::Dimension::Auto, + stylo::MaxSize::MaxContent => taffy::Dimension::AUTO, + stylo::MaxSize::MinContent => taffy::Dimension::AUTO, + stylo::MaxSize::FitContent => taffy::Dimension::AUTO, + stylo::MaxSize::FitContentFunction(_) => taffy::Dimension::AUTO, + stylo::MaxSize::Stretch => taffy::Dimension::AUTO, // Anchor positioning will be flagged off for time being stylo::MaxSize::AnchorSizeFunction(_) => unreachable!(), @@ -85,7 +87,7 @@ pub fn max_size_dimension(val: &stylo::MaxSize) -> taffy::Dimension { #[inline] pub fn margin(val: &stylo::MarginVal) -> taffy::LengthPercentageAuto { match val { - stylo::MarginVal::Auto => taffy::LengthPercentageAuto::Auto, + stylo::MarginVal::Auto => taffy::LengthPercentageAuto::AUTO, stylo::MarginVal::LengthPercentage(val) => length_percentage(val).into(), // Anchor positioning will be flagged off for time being @@ -97,7 +99,7 @@ pub fn margin(val: &stylo::MarginVal) -> taffy::LengthPercentageAuto { #[inline] pub fn inset(val: &stylo::InsetVal) -> taffy::LengthPercentageAuto { match val { - stylo::InsetVal::Auto => taffy::LengthPercentageAuto::Auto, + stylo::InsetVal::Auto => taffy::LengthPercentageAuto::AUTO, stylo::InsetVal::LengthPercentage(val) => length_percentage(val).into(), // Anchor positioning will be flagged off for time being @@ -214,7 +216,7 @@ pub fn gap(input: &stylo::Gap) -> taffy::LengthPercentage { match input { // For Flexbox and CSS Grid the "normal" value is 0px. This may need to be updated // if we ever implement multi-column layout. - stylo::Gap::Normal => taffy::LengthPercentage::Length(0.0), + stylo::Gap::Normal => taffy::LengthPercentage::ZERO, stylo::Gap::LengthPercentage(val) => length_percentage(&val.0), } } @@ -306,16 +308,18 @@ pub fn track_size( max: max_track(max), }, stylo::TrackSize::FitContent(limit) => taffy::MinMax { - min: taffy::MinTrackSizingFunction::Auto, - max: taffy::MaxTrackSizingFunction::FitContent(match limit { - stylo::TrackBreadth::Breadth(lp) => length_percentage(lp), + min: taffy::MinTrackSizingFunction::AUTO, + max: match limit { + stylo::TrackBreadth::Breadth(lp) => { + MaxTrackSizingFunction::fit_content(length_percentage(lp)) + }, // Are these valid? Taffy doesn't support this in any case stylo::TrackBreadth::Fr(_) => unreachable!(), stylo::TrackBreadth::Auto => unreachable!(), stylo::TrackBreadth::MinContent => unreachable!(), stylo::TrackBreadth::MaxContent => unreachable!(), - }), + }, }, } } @@ -325,13 +329,11 @@ pub fn min_track( input: &stylo::TrackBreadth<stylo::LengthPercentage>, ) -> taffy::MinTrackSizingFunction { match input { - stylo::TrackBreadth::Breadth(lp) => { - taffy::MinTrackSizingFunction::Fixed(length_percentage(lp)) - }, - stylo::TrackBreadth::Fr(_) => taffy::MinTrackSizingFunction::Auto, - stylo::TrackBreadth::Auto => taffy::MinTrackSizingFunction::Auto, - stylo::TrackBreadth::MinContent => taffy::MinTrackSizingFunction::MinContent, - stylo::TrackBreadth::MaxContent => taffy::MinTrackSizingFunction::MaxContent, + stylo::TrackBreadth::Breadth(lp) => length_percentage(lp).into(), + stylo::TrackBreadth::Fr(_) => taffy::MinTrackSizingFunction::AUTO, + stylo::TrackBreadth::Auto => taffy::MinTrackSizingFunction::AUTO, + stylo::TrackBreadth::MinContent => taffy::MinTrackSizingFunction::MIN_CONTENT, + stylo::TrackBreadth::MaxContent => taffy::MinTrackSizingFunction::MAX_CONTENT, } } @@ -340,12 +342,10 @@ pub fn max_track( input: &stylo::TrackBreadth<stylo::LengthPercentage>, ) -> taffy::MaxTrackSizingFunction { match input { - stylo::TrackBreadth::Breadth(lp) => { - taffy::MaxTrackSizingFunction::Fixed(length_percentage(lp)) - }, - stylo::TrackBreadth::Fr(val) => taffy::MaxTrackSizingFunction::Fraction(*val), - stylo::TrackBreadth::Auto => taffy::MaxTrackSizingFunction::Auto, - stylo::TrackBreadth::MinContent => taffy::MaxTrackSizingFunction::MinContent, - stylo::TrackBreadth::MaxContent => taffy::MaxTrackSizingFunction::MaxContent, + stylo::TrackBreadth::Breadth(lp) => length_percentage(lp).into(), + stylo::TrackBreadth::Fr(val) => fr(*val), + stylo::TrackBreadth::Auto => taffy::MaxTrackSizingFunction::AUTO, + stylo::TrackBreadth::MinContent => taffy::MaxTrackSizingFunction::MIN_CONTENT, + stylo::TrackBreadth::MaxContent => taffy::MaxTrackSizingFunction::MAX_CONTENT, } } diff --git a/components/layout/taffy/stylo_taffy/wrapper.rs b/components/layout/taffy/stylo_taffy/wrapper.rs index 35b19aa7838..22d02ffeb08 100644 --- a/components/layout/taffy/stylo_taffy/wrapper.rs +++ b/components/layout/taffy/stylo_taffy/wrapper.rs @@ -10,33 +10,44 @@ use super::convert; /// A wrapper struct for anything that Deref's to a [`ComputedValues`], which /// implements Taffy's layout traits and can used with Taffy's layout algorithms. -pub struct TaffyStyloStyle<T: Deref<Target = ComputedValues>>(pub T); +pub struct TaffyStyloStyle<T: Deref<Target = ComputedValues>> { + pub style: T, + pub is_compressible_replaced: bool, +} -impl<T: Deref<Target = ComputedValues>> From<T> for TaffyStyloStyle<T> { - fn from(value: T) -> Self { - Self(value) +impl<T: Deref<Target = ComputedValues>> TaffyStyloStyle<T> { + pub fn new(style: T, is_compressible_replaced: bool) -> Self { + Self { + style, + is_compressible_replaced, + } } } impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> { #[inline] fn box_generation_mode(&self) -> taffy::BoxGenerationMode { - convert::box_generation_mode(self.0.get_box().display) + convert::box_generation_mode(self.style.get_box().display) } #[inline] fn is_block(&self) -> bool { - convert::is_block(self.0.get_box().display) + convert::is_block(self.style.get_box().display) + } + + #[inline] + fn is_compressible_replaced(&self) -> bool { + self.is_compressible_replaced } #[inline] fn box_sizing(&self) -> taffy::BoxSizing { - convert::box_sizing(self.0.get_position().box_sizing) + convert::box_sizing(self.style.get_position().box_sizing) } #[inline] fn overflow(&self) -> taffy::Point<taffy::Overflow> { - let box_styles = self.0.get_box(); + let box_styles = self.style.get_box(); taffy::Point { x: convert::overflow(box_styles.overflow_x), y: convert::overflow(box_styles.overflow_y), @@ -50,12 +61,12 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn position(&self) -> taffy::Position { - convert::position(self.0.get_box().position) + convert::position(self.style.get_box().position) } #[inline] fn inset(&self) -> taffy::Rect<taffy::LengthPercentageAuto> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Rect { left: convert::inset(&position_styles.left), right: convert::inset(&position_styles.right), @@ -66,7 +77,7 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn size(&self) -> taffy::Size<taffy::Dimension> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Size { width: convert::dimension(&position_styles.width), height: convert::dimension(&position_styles.height), @@ -75,7 +86,7 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn min_size(&self) -> taffy::Size<taffy::Dimension> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Size { width: convert::dimension(&position_styles.min_width), height: convert::dimension(&position_styles.min_height), @@ -84,7 +95,7 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn max_size(&self) -> taffy::Size<taffy::Dimension> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Size { width: convert::max_size_dimension(&position_styles.max_width), height: convert::max_size_dimension(&position_styles.max_height), @@ -93,12 +104,12 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn aspect_ratio(&self) -> Option<f32> { - convert::aspect_ratio(self.0.get_position().aspect_ratio) + convert::aspect_ratio(self.style.get_position().aspect_ratio) } #[inline] fn margin(&self) -> taffy::Rect<taffy::LengthPercentageAuto> { - let margin_styles = self.0.get_margin(); + let margin_styles = self.style.get_margin(); taffy::Rect { left: convert::margin(&margin_styles.margin_left), right: convert::margin(&margin_styles.margin_right), @@ -109,7 +120,7 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn padding(&self) -> taffy::Rect<taffy::LengthPercentage> { - let padding_styles = self.0.get_padding(); + let padding_styles = self.style.get_padding(); taffy::Rect { left: convert::length_percentage(&padding_styles.padding_left.0), right: convert::length_percentage(&padding_styles.padding_right.0), @@ -120,12 +131,12 @@ impl<T: Deref<Target = ComputedValues>> taffy::CoreStyle for TaffyStyloStyle<T> #[inline] fn border(&self) -> taffy::Rect<taffy::LengthPercentage> { - let border_styles = self.0.get_border(); + let border_styles = self.style.get_border(); taffy::Rect { - left: taffy::LengthPercentage::Length(border_styles.border_left_width.to_f32_px()), - right: taffy::LengthPercentage::Length(border_styles.border_right_width.to_f32_px()), - top: taffy::LengthPercentage::Length(border_styles.border_top_width.to_f32_px()), - bottom: taffy::LengthPercentage::Length(border_styles.border_bottom_width.to_f32_px()), + left: taffy::LengthPercentage::length(border_styles.border_left_width.to_f32_px()), + right: taffy::LengthPercentage::length(border_styles.border_right_width.to_f32_px()), + top: taffy::LengthPercentage::length(border_styles.border_top_width.to_f32_px()), + bottom: taffy::LengthPercentage::length(border_styles.border_bottom_width.to_f32_px()), } } } @@ -142,32 +153,32 @@ impl<T: Deref<Target = ComputedValues>> taffy::GridContainerStyle for TaffyStylo #[inline] fn grid_template_rows(&self) -> Self::TemplateTrackList<'_> { - convert::grid_template_tracks(&self.0.get_position().grid_template_rows) + convert::grid_template_tracks(&self.style.get_position().grid_template_rows) } #[inline] fn grid_template_columns(&self) -> Self::TemplateTrackList<'_> { - convert::grid_template_tracks(&self.0.get_position().grid_template_columns) + convert::grid_template_tracks(&self.style.get_position().grid_template_columns) } #[inline] fn grid_auto_rows(&self) -> Self::AutoTrackList<'_> { - convert::grid_auto_tracks(&self.0.get_position().grid_auto_rows) + convert::grid_auto_tracks(&self.style.get_position().grid_auto_rows) } #[inline] fn grid_auto_columns(&self) -> Self::AutoTrackList<'_> { - convert::grid_auto_tracks(&self.0.get_position().grid_auto_columns) + convert::grid_auto_tracks(&self.style.get_position().grid_auto_columns) } #[inline] fn grid_auto_flow(&self) -> taffy::GridAutoFlow { - convert::grid_auto_flow(self.0.get_position().grid_auto_flow) + convert::grid_auto_flow(self.style.get_position().grid_auto_flow) } #[inline] fn gap(&self) -> taffy::Size<taffy::LengthPercentage> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Size { width: convert::gap(&position_styles.column_gap), height: convert::gap(&position_styles.row_gap), @@ -176,29 +187,29 @@ impl<T: Deref<Target = ComputedValues>> taffy::GridContainerStyle for TaffyStylo #[inline] fn align_content(&self) -> Option<taffy::AlignContent> { - convert::content_alignment(self.0.get_position().align_content.0) + convert::content_alignment(self.style.get_position().align_content.0) } #[inline] fn justify_content(&self) -> Option<taffy::JustifyContent> { - convert::content_alignment(self.0.get_position().justify_content.0) + convert::content_alignment(self.style.get_position().justify_content.0) } #[inline] fn align_items(&self) -> Option<taffy::AlignItems> { - convert::item_alignment(self.0.get_position().align_items.0) + convert::item_alignment(self.style.get_position().align_items.0) } #[inline] fn justify_items(&self) -> Option<taffy::AlignItems> { - convert::item_alignment(self.0.get_position().justify_items.computed.0) + convert::item_alignment(self.style.get_position().justify_items.computed.0) } } impl<T: Deref<Target = ComputedValues>> taffy::GridItemStyle for TaffyStyloStyle<T> { #[inline] fn grid_row(&self) -> taffy::Line<taffy::GridPlacement> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Line { start: convert::grid_line(&position_styles.grid_row_start), end: convert::grid_line(&position_styles.grid_row_end), @@ -207,7 +218,7 @@ impl<T: Deref<Target = ComputedValues>> taffy::GridItemStyle for TaffyStyloStyle #[inline] fn grid_column(&self) -> taffy::Line<taffy::GridPlacement> { - let position_styles = self.0.get_position(); + let position_styles = self.style.get_position(); taffy::Line { start: convert::grid_line(&position_styles.grid_column_start), end: convert::grid_line(&position_styles.grid_column_end), @@ -216,11 +227,11 @@ impl<T: Deref<Target = ComputedValues>> taffy::GridItemStyle for TaffyStyloStyle #[inline] fn align_self(&self) -> Option<taffy::AlignSelf> { - convert::item_alignment(self.0.get_position().align_self.0.0) + convert::item_alignment(self.style.get_position().align_self.0.0) } #[inline] fn justify_self(&self) -> Option<taffy::AlignSelf> { - convert::item_alignment(self.0.get_position().justify_self.0.0) + convert::item_alignment(self.style.get_position().justify_self.0.0) } } diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 33d0da952fb..e99776be350 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -170,8 +170,12 @@ pub async fn fetch_with_cors_cache( // TODO: We don't implement fetchParams as defined in the spec } -fn convert_request_to_csp_request(request: &Request, origin: &ImmutableOrigin) -> csp::Request { - csp::Request { +pub(crate) fn convert_request_to_csp_request(request: &Request) -> Option<csp::Request> { + let origin = match &request.origin { + Origin::Client => return None, + Origin::Origin(origin) => origin, + }; + let csp_request = csp::Request { url: request.url().into_url(), origin: origin.clone().into_url_origin(), redirect_count: request.redirect_count, @@ -190,45 +194,58 @@ fn convert_request_to_csp_request(request: &Request, origin: &ImmutableOrigin) - ParserMetadata::NotParserInserted => csp::ParserMetadata::NotParserInserted, ParserMetadata::Default => csp::ParserMetadata::None, }, - } + }; + Some(csp_request) } /// <https://www.w3.org/TR/CSP/#should-block-request> pub fn should_request_be_blocked_by_csp( - request: &Request, + csp_request: &csp::Request, policy_container: &PolicyContainer, ) -> (csp::CheckResult, Vec<csp::Violation>) { - let origin = match &request.origin { - Origin::Client => return (csp::CheckResult::Allowed, Vec::new()), - Origin::Origin(origin) => origin, - }; - let csp_request = convert_request_to_csp_request(request, origin); - policy_container .csp_list .as_ref() - .map(|c| c.should_request_be_blocked(&csp_request)) + .map(|c| c.should_request_be_blocked(csp_request)) .unwrap_or((csp::CheckResult::Allowed, Vec::new())) } /// <https://www.w3.org/TR/CSP/#report-for-request> pub fn report_violations_for_request_by_csp( - request: &Request, + csp_request: &csp::Request, policy_container: &PolicyContainer, ) -> Vec<csp::Violation> { - let origin = match &request.origin { - Origin::Client => return Vec::new(), - Origin::Origin(origin) => origin, - }; - let csp_request = convert_request_to_csp_request(request, origin); - policy_container .csp_list .as_ref() - .map(|c| c.report_violations_for_request(&csp_request)) + .map(|c| c.report_violations_for_request(csp_request)) .unwrap_or_default() } +fn should_response_be_blocked_by_csp( + csp_request: &csp::Request, + response: &Response, + policy_container: &PolicyContainer, +) -> (csp::CheckResult, Vec<csp::Violation>) { + if response.is_network_error() { + return (csp::CheckResult::Allowed, Vec::new()); + } + let csp_response = csp::Response { + url: response + .actual_response() + .url() + .cloned() + .expect("response must have a url") + .into_url(), + redirect_count: csp_request.redirect_count, + }; + policy_container + .csp_list + .as_ref() + .map(|c| c.should_response_to_request_be_blocked(csp_request, &csp_response)) + .unwrap_or((csp::CheckResult::Allowed, Vec::new())) +} + /// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) pub async fn main_fetch( fetch_params: &mut FetchParams, @@ -270,13 +287,15 @@ pub async fn main_fetch( RequestPolicyContainer::Client => PolicyContainer::default(), RequestPolicyContainer::PolicyContainer(container) => container.to_owned(), }; + let csp_request = convert_request_to_csp_request(request); + if let Some(csp_request) = csp_request.as_ref() { + // Step 2.2. + let violations = report_violations_for_request_by_csp(csp_request, &policy_container); - // Step 2.2. - let violations = report_violations_for_request_by_csp(request, &policy_container); - - if !violations.is_empty() { - target.process_csp_violations(request, violations); - } + if !violations.is_empty() { + target.process_csp_violations(request, violations); + } + }; // Step 3. // TODO: handle request abort. @@ -309,22 +328,24 @@ pub async fn main_fetch( request.insecure_requests_policy ); } + if let Some(csp_request) = csp_request.as_ref() { + // Step 7. If should request be blocked due to a bad port, should fetching request be blocked + // as mixed content, or should request be blocked by Content Security Policy returns blocked, + // then set response to a network error. + let (check_result, violations) = + should_request_be_blocked_by_csp(csp_request, &policy_container); + + if !violations.is_empty() { + target.process_csp_violations(request, violations); + } - // Step 7. If should request be blocked due to a bad port, should fetching request be blocked - // as mixed content, or should request be blocked by Content Security Policy returns blocked, - // then set response to a network error. - let (check_result, violations) = should_request_be_blocked_by_csp(request, &policy_container); - - if !violations.is_empty() { - target.process_csp_violations(request, violations); - } - - if check_result == csp::CheckResult::Blocked { - warn!("Request blocked by CSP"); - response = Some(Response::network_error(NetworkError::Internal( - "Blocked by Content-Security-Policy".into(), - ))) - } + if check_result == csp::CheckResult::Blocked { + warn!("Request blocked by CSP"); + response = Some(Response::network_error(NetworkError::Internal( + "Blocked by Content-Security-Policy".into(), + ))) + } + }; if should_request_be_blocked_due_to_a_bad_port(&request.current_url()) { response = Some(Response::network_error(NetworkError::Internal( "Request attempted on bad port".into(), @@ -530,6 +551,14 @@ pub async fn main_fetch( should_be_blocked_due_to_mime_type(request.destination, &response.headers); let should_replace_with_mixed_content = !response_is_network_error && should_response_be_blocked_as_mixed_content(request, &response, &context.protocols); + let should_replace_with_csp_error = csp_request.is_some_and(|csp_request| { + let (check_result, violations) = + should_response_be_blocked_by_csp(&csp_request, &response, &policy_container); + if !violations.is_empty() { + target.process_csp_violations(request, violations); + } + check_result == csp::CheckResult::Blocked + }); // Step 15. let mut network_error_response = response @@ -553,7 +582,7 @@ pub async fn main_fetch( // Step 19. If response is not a network error and any of the following returns blocked // * should internalResponse to request be blocked as mixed content - // TODO: * should internalResponse to request be blocked by Content Security Policy + // * should internalResponse to request be blocked by Content Security Policy // * should internalResponse to request be blocked due to its MIME type // * should internalResponse to request be blocked due to nosniff let mut blocked_error_response; @@ -572,6 +601,10 @@ pub async fn main_fetch( blocked_error_response = Response::network_error(NetworkError::Internal("Blocked as mixed content".into())); &blocked_error_response + } else if should_replace_with_csp_error { + blocked_error_response = + Response::network_error(NetworkError::Internal("Blocked due to CSP".into())); + &blocked_error_response } else { internal_response }; diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 704901f6940..3e6c2bc0695 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -59,7 +59,7 @@ use net_traits::{ use profile_traits::mem::{Report, ReportKind}; use profile_traits::path; use servo_arc::Arc; -use servo_url::{ImmutableOrigin, ServoUrl}; +use servo_url::{Host, ImmutableOrigin, ServoUrl}; use tokio::sync::mpsc::{ Receiver as TokioReceiver, Sender as TokioSender, UnboundedReceiver, UnboundedSender, channel, unbounded_channel, @@ -223,8 +223,11 @@ fn strict_origin_when_cross_origin( strip_url_for_use_as_referrer(referrer_url, true) } -/// <https://html.spec.whatwg.org/multipage/#concept-site-same-site> +/// <https://html.spec.whatwg.org/multipage/#same-site> fn is_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool { + // First steps are for + // https://html.spec.whatwg.org/multipage/#concept-site-same-site + // // Step 1. If A and B are the same opaque origin, then return true. if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b { return true; @@ -244,7 +247,12 @@ fn is_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool { } // Step 4. If A's and B's host values are not equal, then return false. - if host_a != host_b { + // Includes the steps of https://html.spec.whatwg.org/multipage/#obtain-a-site + if let (Host::Domain(domain_a), Host::Domain(domain_b)) = (host_a, host_b) { + if reg_suffix(domain_a) != reg_suffix(domain_b) { + return false; + } + } else if host_a != host_b { return false; } @@ -2564,7 +2572,7 @@ fn set_the_sec_fetch_site_header(r: &mut Request) { header = SecFetchSite::CrossSite; // Step 5.3 If r’s origin is not same site with url’s origin, then break. - if is_same_site(request_origin, &url.origin()) { + if !is_same_site(request_origin, &url.origin()) { break; } diff --git a/components/net/lib.rs b/components/net/lib.rs index 0576c017e59..b236f2bc784 100644 --- a/components/net/lib.rs +++ b/components/net/lib.rs @@ -16,7 +16,6 @@ pub mod http_cache; pub mod http_loader; pub mod image_cache; pub mod local_directory_listing; -pub mod mime_classifier; pub mod protocols; pub mod request_interceptor; pub mod resource_thread; diff --git a/components/net/tests/fetch.rs b/components/net/tests/fetch.rs index e8c5077f12a..0deceab3055 100644 --- a/components/net/tests/fetch.rs +++ b/components/net/tests/fetch.rs @@ -225,7 +225,7 @@ fn test_fetch_blob() { #[test] fn test_file() { - let path = Path::new("../../resources/ahem.css") + let path = Path::new("../../components/net/tests/test.css") .canonicalize() .unwrap(); let url = ServoUrl::from_file_path(path.clone()).unwrap(); diff --git a/components/net/tests/http_loader.rs b/components/net/tests/http_loader.rs index b1e90276472..494d14cb281 100644 --- a/components/net/tests/http_loader.rs +++ b/components/net/tests/http_loader.rs @@ -31,8 +31,6 @@ use http::{HeaderName, Method, StatusCode}; use http_body_util::combinators::BoxBody; use hyper::body::{Body, Bytes, Incoming}; use hyper::{Request as HyperRequest, Response as HyperResponse}; -use ipc_channel::ipc::{self, IpcSharedMemory}; -use ipc_channel::router::ROUTER; use net::cookie::ServoCookie; use net::cookie_storage::CookieStorage; use net::fetch::methods::{self}; @@ -41,8 +39,8 @@ use net::resource_thread::AuthCacheEntry; use net::test::{DECODER_BUFFER_SIZE, replace_host_table}; use net_traits::http_status::HttpStatus; use net_traits::request::{ - BodyChunkRequest, BodyChunkResponse, BodySource, CredentialsMode, Destination, Referrer, - Request, RequestBody, RequestBuilder, RequestMode, + CredentialsMode, Destination, Referrer, Request, RequestBuilder, RequestMode, + create_request_body_with_content, }; use net_traits::response::{Response, ResponseBody}; use net_traits::{CookieSource, FetchTaskTarget, NetworkError, ReferrerPolicy}; @@ -100,24 +98,6 @@ pub fn expect_devtools_http_response( } } -fn create_request_body_with_content(content: IpcSharedMemory) -> RequestBody { - let content_len = content.len(); - - let (chunk_request_sender, chunk_request_receiver) = ipc::channel().unwrap(); - ROUTER.add_typed_route( - chunk_request_receiver, - Box::new(move |message| { - let request = message.unwrap(); - if let BodyChunkRequest::Connect(sender) = request { - let _ = sender.send(BodyChunkResponse::Chunk(content.clone())); - let _ = sender.send(BodyChunkResponse::Done); - } - }), - ); - - RequestBody::new(chunk_request_sender, BodySource::Object, Some(content_len)) -} - #[test] fn test_check_default_headers_loaded_in_every_request() { let expected_headers = Arc::new(Mutex::new(None)); @@ -329,7 +309,7 @@ fn test_request_and_response_data_with_network_messages() { ); headers.insert( HeaderName::from_static("sec-fetch-site"), - HeaderValue::from_static("same-site"), + HeaderValue::from_static("cross-site"), ); headers.insert( HeaderName::from_static("sec-fetch-user"), @@ -591,8 +571,8 @@ fn test_load_doesnt_send_request_body_on_any_redirect() { }; let (pre_server, pre_url) = make_server(pre_handler); - let content = b"Body on POST!"; - let request_body = create_request_body_with_content(IpcSharedMemory::from_bytes(content)); + let content = "Body on POST!"; + let request_body = create_request_body_with_content(content); let request = RequestBuilder::new(None, pre_url.clone(), Referrer::NoReferrer) .body(Some(request_body)) @@ -891,20 +871,21 @@ fn test_when_cookie_received_marked_secure_is_ignored_for_http() { #[test] fn test_load_sets_content_length_to_length_of_request_body() { - let content = b"This is a request body"; + let content = "This is a request body"; + let content_bytes = content.as_bytes(); let handler = move |request: HyperRequest<Incoming>, response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| { - let content_length = ContentLength(content.len() as u64); + let content_length = ContentLength(content_bytes.len() as u64); assert_eq!( request.headers().typed_get::<ContentLength>(), Some(content_length) ); - *response.body_mut() = make_body(content.to_vec()); + *response.body_mut() = make_body(content_bytes.to_vec()); }; let (server, url) = make_server(handler); - let request_body = create_request_body_with_content(IpcSharedMemory::from_bytes(content)); + let request_body = create_request_body_with_content(content); let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer) .method(Method::POST) diff --git a/components/net/tests/main.rs b/components/net/tests/main.rs index ba58c4a4c1d..77bc11d4c61 100644 --- a/components/net/tests/main.rs +++ b/components/net/tests/main.rs @@ -14,7 +14,6 @@ mod filemanager_thread; mod hsts; mod http_cache; mod http_loader; -mod mime_classifier; mod resource_thread; mod subresource_integrity; diff --git a/components/net/tests/test.css b/components/net/tests/test.css new file mode 100644 index 00000000000..f7fa63bbaa7 --- /dev/null +++ b/components/net/tests/test.css @@ -0,0 +1,3 @@ +html { + color: red; +} diff --git a/components/net/websocket_loader.rs b/components/net/websocket_loader.rs index 128436ac47c..0b5a81f47ae 100644 --- a/components/net/websocket_loader.rs +++ b/components/net/websocket_loader.rs @@ -43,7 +43,8 @@ use crate::async_runtime::HANDLE; use crate::connector::{CACertificates, TlsConfig, create_tls_config}; use crate::cookie::ServoCookie; use crate::fetch::methods::{ - should_request_be_blocked_by_csp, should_request_be_blocked_due_to_a_bad_port, + convert_request_to_csp_request, should_request_be_blocked_by_csp, + should_request_be_blocked_due_to_a_bad_port, }; use crate::hosts::replace_host; use crate::http_loader::HttpState; @@ -168,12 +169,12 @@ fn setup_dom_listener( trace!("handling WS DOM action: {:?}", dom_action); match dom_action { WebSocketDomAction::SendMessage(MessageData::Text(data)) => { - if let Err(e) = sender.send(DomMsg::Send(Message::Text(data))) { + if let Err(e) = sender.send(DomMsg::Send(Message::Text(data.into()))) { warn!("Error sending websocket message: {:?}", e); } }, WebSocketDomAction::SendMessage(MessageData::Binary(data)) => { - if let Err(e) = sender.send(DomMsg::Send(Message::Binary(data))) { + if let Err(e) = sender.send(DomMsg::Send(Message::Binary(data.into()))) { warn!("Error sending websocket message: {:?}", e); } }, @@ -245,7 +246,7 @@ async fn run_ws_loop( }; match msg { Message::Text(s) => { - let message = MessageData::Text(s); + let message = MessageData::Text(s.as_str().to_owned()); if let Err(e) = resource_event_sender .send(WebSocketNetworkEvent::MessageReceived(message)) { @@ -255,7 +256,7 @@ async fn run_ws_loop( } Message::Binary(v) => { - let message = MessageData::Binary(v); + let message = MessageData::Binary(v.to_vec()); if let Err(e) = resource_event_sender .send(WebSocketNetworkEvent::MessageReceived(message)) { @@ -390,14 +391,18 @@ fn connect( RequestPolicyContainer::PolicyContainer(container) => container.to_owned(), }; - let (check_result, violations) = should_request_be_blocked_by_csp(&request, &policy_container); + if let Some(csp_request) = convert_request_to_csp_request(&request) { + let (check_result, violations) = + should_request_be_blocked_by_csp(&csp_request, &policy_container); - if !violations.is_empty() { - let _ = resource_event_sender.send(WebSocketNetworkEvent::ReportCSPViolations(violations)); - } + if !violations.is_empty() { + let _ = + resource_event_sender.send(WebSocketNetworkEvent::ReportCSPViolations(violations)); + } - if check_result == csp::CheckResult::Blocked { - return Err("Blocked by Content-Security-Policy".to_string()); + if check_result == csp::CheckResult::Blocked { + return Err("Blocked by Content-Security-Policy".to_string()); + } } let client = match create_request( diff --git a/components/pixels/lib.rs b/components/pixels/lib.rs index 5978a7ddde3..3dda4d28bde 100644 --- a/components/pixels/lib.rs +++ b/components/pixels/lib.rs @@ -31,7 +31,21 @@ pub enum PixelFormat { BGRA8, } -pub fn rgba8_get_rect(pixels: &[u8], size: Size2D<u64>, rect: Rect<u64>) -> Cow<[u8]> { +// Computes image byte length, returning None if overflow occurred or the total length exceeds +// the maximum image allocation size. +pub fn compute_rgba8_byte_length_if_within_limit(width: usize, height: usize) -> Option<usize> { + // Maximum allowed image allocation size (2^31-1 ~ 2GB). + const MAX_IMAGE_BYTE_LENGTH: usize = 2147483647; + + // The color components of each pixel must be stored in four sequential + // elements in the order of red, green, blue, and then alpha. + 4usize + .checked_mul(width) + .and_then(|v| v.checked_mul(height)) + .filter(|v| *v <= MAX_IMAGE_BYTE_LENGTH) +} + +pub fn rgba8_get_rect(pixels: &[u8], size: Size2D<u32>, rect: Rect<u32>) -> Cow<[u8]> { assert!(!rect.is_empty()); assert!(Rect::from_size(size).contains_rect(&rect)); assert_eq!(pixels.len() % 4, 0); @@ -92,18 +106,18 @@ pub fn multiply_u8_color(a: u8, b: u8) -> u8 { pub fn clip( mut origin: Point2D<i32>, - mut size: Size2D<u64>, - surface: Size2D<u64>, -) -> Option<Rect<u64>> { + mut size: Size2D<u32>, + surface: Size2D<u32>, +) -> Option<Rect<u32>> { if origin.x < 0 { - size.width = size.width.saturating_sub(-origin.x as u64); + size.width = size.width.saturating_sub(-origin.x as u32); origin.x = 0; } if origin.y < 0 { - size.height = size.height.saturating_sub(-origin.y as u64); + size.height = size.height.saturating_sub(-origin.y as u32); origin.y = 0; } - let origin = Point2D::new(origin.x as u64, origin.y as u64); + let origin = Point2D::new(origin.x as u32, origin.y as u32); Rect::new(origin, size) .intersection(&Rect::from_size(surface)) .filter(|rect| !rect.is_empty()) diff --git a/components/profile/mem.rs b/components/profile/mem.rs index b9920f252b4..0aff98b89d0 100644 --- a/components/profile/mem.rs +++ b/components/profile/mem.rs @@ -114,7 +114,6 @@ impl Profiler { let _ = sender.send(MemoryReportResult { results }); true }, - ProfilerMsg::Exit => false, } } diff --git a/components/script/body.rs b/components/script/body.rs index cc7870a0845..ee269fd430a 100644 --- a/components/script/body.rs +++ b/components/script/body.rs @@ -249,7 +249,7 @@ impl TransmitBodyConnectHandler { let rejection_handler = Box::new(TransmitBodyPromiseRejectionHandler { bytes_sender, - stream: rooted_stream, + stream: Dom::from_ref(&rooted_stream.clone()), control_sender, }); @@ -321,11 +321,12 @@ impl Callback for TransmitBodyPromiseHandler { /// The handler of read promises rejection of body streams used in /// <https://fetch.spec.whatwg.org/#concept-request-transmit-body>. #[derive(Clone, JSTraceable, MallocSizeOf)] +#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] struct TransmitBodyPromiseRejectionHandler { #[ignore_malloc_size_of = "Channels are hard"] #[no_trace] bytes_sender: IpcSender<BodyChunkResponse>, - stream: DomRoot<ReadableStream>, + stream: Dom<ReadableStream>, #[ignore_malloc_size_of = "Channels are hard"] #[no_trace] control_sender: IpcSender<BodyChunkRequest>, diff --git a/components/script/canvas_context.rs b/components/script/canvas_context.rs index ec388e039f1..aadbd656a0c 100644 --- a/components/script/canvas_context.rs +++ b/components/script/canvas_context.rs @@ -48,7 +48,7 @@ pub(crate) trait CanvasContext { true } - fn size(&self) -> Size2D<u64> { + fn size(&self) -> Size2D<u32> { self.canvas().size() } @@ -73,12 +73,12 @@ pub(crate) trait CanvasContext { } pub(crate) trait CanvasHelpers { - fn size(&self) -> Size2D<u64>; + fn size(&self) -> Size2D<u32>; fn canvas(&self) -> Option<&HTMLCanvasElement>; } impl CanvasHelpers for HTMLCanvasElementOrOffscreenCanvas { - fn size(&self) -> Size2D<u64> { + fn size(&self) -> Size2D<u32> { match self { HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(canvas) => { canvas.get_size().cast() @@ -164,7 +164,7 @@ impl CanvasContext for RenderingContext { } } - fn size(&self) -> Size2D<u64> { + fn size(&self) -> Size2D<u32> { match self { RenderingContext::Placeholder(context) => (*context.context().unwrap()).size(), RenderingContext::Context2d(context) => context.size(), @@ -251,7 +251,7 @@ impl CanvasContext for OffscreenRenderingContext { } } - fn size(&self) -> Size2D<u64> { + fn size(&self) -> Size2D<u32> { match self { OffscreenRenderingContext::Context2d(context) => context.size(), } diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index d788d2f5929..7f77daf513b 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -55,6 +55,7 @@ use crate::dom::element::{Element, cors_setting_for_element}; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlcanvaselement::HTMLCanvasElement; use crate::dom::htmlvideoelement::HTMLVideoElement; +use crate::dom::imagebitmap::ImageBitmap; use crate::dom::imagedata::ImageData; use crate::dom::node::{Node, NodeTraits}; use crate::dom::offscreencanvas::OffscreenCanvas; @@ -319,6 +320,7 @@ impl CanvasState { }, CanvasImageSource::HTMLVideoElement(video) => video.origin_is_clean(), CanvasImageSource::HTMLCanvasElement(canvas) => canvas.origin_is_clean(), + CanvasImageSource::ImageBitmap(bitmap) => bitmap.origin_is_clean(), CanvasImageSource::OffscreenCanvas(canvas) => canvas.origin_is_clean(), CanvasImageSource::CSSStyleValue(_) => true, } @@ -383,7 +385,7 @@ impl CanvasState { } } - pub(crate) fn get_rect(&self, canvas_size: Size2D<u64>, rect: Rect<u64>) -> Vec<u8> { + pub(crate) fn get_rect(&self, canvas_size: Size2D<u32>, rect: Rect<u32>) -> Vec<u8> { assert!(self.origin_is_clean()); assert!(Rect::from_size(canvas_size).contains_rect(&rect)); @@ -459,6 +461,15 @@ impl CanvasState { self.draw_html_canvas_element(canvas, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh) }, + CanvasImageSource::ImageBitmap(ref bitmap) => { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if bitmap.is_detached() { + return Err(Error::InvalidState); + } + + self.draw_image_bitmap(bitmap, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh); + Ok(()) + }, CanvasImageSource::OffscreenCanvas(ref canvas) => { // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> if canvas.get_size().is_empty() { @@ -531,11 +542,11 @@ impl CanvasState { }; // Step 4. Establish the source and destination rectangles. - let video_size = snapshot.size().to_f64(); - let dw = dw.unwrap_or(video_size.width); - let dh = dh.unwrap_or(video_size.height); - let sw = sw.unwrap_or(video_size.width); - let sh = sh.unwrap_or(video_size.height); + let video_size = snapshot.size(); + let dw = dw.unwrap_or(video_size.width as f64); + let dh = dh.unwrap_or(video_size.height as f64); + let sw = sw.unwrap_or(video_size.width as f64); + let sh = sh.unwrap_or(video_size.height as f64); let (source_rect, dest_rect) = self.adjust_source_dest_rects(video_size, sx, sy, sw, sh, dx, dy, dw, dh); @@ -577,7 +588,7 @@ impl CanvasState { let sw = sw.unwrap_or(canvas_size.width as f64); let sh = sh.unwrap_or(canvas_size.height as f64); - let image_size = Size2D::new(canvas_size.width as f64, canvas_size.height as f64); + let image_size = Size2D::new(canvas_size.width, canvas_size.height); // 2. Establish the source and destination rectangles let (source_rect, dest_rect) = self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh); @@ -632,7 +643,7 @@ impl CanvasState { let sw = sw.unwrap_or(canvas_size.width as f64); let sh = sh.unwrap_or(canvas_size.height as f64); - let image_size = Size2D::new(canvas_size.width as f64, canvas_size.height as f64); + let image_size = Size2D::new(canvas_size.width, canvas_size.height); // 2. Establish the source and destination rectangles let (source_rect, dest_rect) = self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh); @@ -702,12 +713,12 @@ impl CanvasState { let snapshot = self .fetch_image_data(url, cors_setting) .ok_or(Error::InvalidState)?; - let image_size = snapshot.size().to_f64(); + let image_size = snapshot.size(); - let dw = dw.unwrap_or(image_size.width); - let dh = dh.unwrap_or(image_size.height); - let sw = sw.unwrap_or(image_size.width); - let sh = sh.unwrap_or(image_size.height); + let dw = dw.unwrap_or(image_size.width as f64); + let dh = dh.unwrap_or(image_size.height as f64); + let sw = sw.unwrap_or(image_size.width as f64); + let sh = sh.unwrap_or(image_size.height as f64); // Establish the source and destination rectangles let (source_rect, dest_rect) = @@ -728,6 +739,52 @@ impl CanvasState { Ok(()) } + /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage> + #[allow(clippy::too_many_arguments)] + fn draw_image_bitmap( + &self, + bitmap: &ImageBitmap, + canvas: Option<&HTMLCanvasElement>, + sx: f64, + sy: f64, + sw: Option<f64>, + sh: Option<f64>, + dx: f64, + dy: f64, + dw: Option<f64>, + dh: Option<f64>, + ) { + let Some(snapshot) = bitmap.bitmap_data().clone() else { + return; + }; + + // Step 4. Establish the source and destination rectangles. + let bitmap_size = snapshot.size(); + let dw = dw.unwrap_or(bitmap_size.width as f64); + let dh = dh.unwrap_or(bitmap_size.height as f64); + let sw = sw.unwrap_or(bitmap_size.width as f64); + let sh = sh.unwrap_or(bitmap_size.height as f64); + + let (source_rect, dest_rect) = + self.adjust_source_dest_rects(bitmap_size, sx, sy, sw, sh, dx, dy, dw, dh); + + // Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted. + if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) { + return; + } + + let smoothing_enabled = self.state.borrow().image_smoothing_enabled; + + self.send_canvas_2d_msg(Canvas2dMsg::DrawImage( + snapshot.as_ipc(), + dest_rect, + source_rect, + smoothing_enabled, + )); + + self.mark_as_dirty(canvas); + } + pub(crate) fn mark_as_dirty(&self, canvas: Option<&HTMLCanvasElement>) { if let Some(canvas) = canvas { canvas.mark_as_dirty(); @@ -741,7 +798,7 @@ impl CanvasState { #[allow(clippy::too_many_arguments)] fn adjust_source_dest_rects( &self, - image_size: Size2D<f64>, + image_size: Size2D<u32>, sx: f64, sy: f64, sw: f64, @@ -766,7 +823,7 @@ impl CanvasState { // When the source rectangle is outside the source image, // the source rectangle must be clipped to the source image let source_rect_clipped = source_rect - .intersection(&image_rect) + .intersection(&image_rect.to_f64()) .unwrap_or(Rect::zero()); // Width and height ratios between the non clipped and clipped source rectangles @@ -1063,6 +1120,14 @@ impl CanvasState { canvas.get_image_data().ok_or(Error::InvalidState)? }, + CanvasImageSource::ImageBitmap(ref bitmap) => { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if bitmap.is_detached() { + return Err(Error::InvalidState); + } + + bitmap.bitmap_data().clone().ok_or(Error::InvalidState)? + }, CanvasImageSource::OffscreenCanvas(ref canvas) => { // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> if canvas.get_size().is_empty() { @@ -1486,7 +1551,7 @@ impl CanvasState { #[allow(clippy::too_many_arguments)] pub(crate) fn get_image_data( &self, - canvas_size: Size2D<u64>, + canvas_size: Size2D<u32>, global: &GlobalScope, sx: i32, sy: i32, @@ -1506,7 +1571,7 @@ impl CanvasState { } let (origin, size) = adjust_size_sign(Point2D::new(sx, sy), Size2D::new(sw, sh)); - let read_rect = match pixels::clip(origin, size.to_u64(), canvas_size) { + let read_rect = match pixels::clip(origin, size.to_u32(), canvas_size) { Some(rect) => rect, None => { // All the pixels are outside the canvas surface. @@ -1526,7 +1591,7 @@ impl CanvasState { // https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata pub(crate) fn put_image_data( &self, - canvas_size: Size2D<u64>, + canvas_size: Size2D<u32>, imagedata: &ImageData, dx: i32, dy: i32, @@ -1547,7 +1612,7 @@ impl CanvasState { #[allow(unsafe_code, clippy::too_many_arguments)] pub(crate) fn put_image_data_( &self, - canvas_size: Size2D<u64>, + canvas_size: Size2D<u32>, imagedata: &ImageData, dx: i32, dy: i32, @@ -1579,7 +1644,7 @@ impl CanvasState { Point2D::new(dirty_x, dirty_y), Size2D::new(dirty_width, dirty_height), ); - let src_rect = match pixels::clip(src_origin, src_size.to_u64(), imagedata_size.to_u64()) { + let src_rect = match pixels::clip(src_origin, src_size.to_u32(), imagedata_size.to_u32()) { Some(rect) => rect, None => return, }; diff --git a/components/script/devtools.rs b/components/script/devtools.rs index 93212887dc8..945470194e2 100644 --- a/components/script/devtools.rs +++ b/components/script/devtools.rs @@ -132,7 +132,7 @@ fn find_node_by_unique_id( document .upcast::<Node>() .traverse_preorder(ShadowIncluding::Yes) - .find(|candidate| candidate.unique_id() == node_id) + .find(|candidate| candidate.unique_id(pipeline) == node_id) }) } diff --git a/components/script/dom/abortcontroller.rs b/components/script/dom/abortcontroller.rs index 3813cfdd51a..3a7ca17220d 100644 --- a/components/script/dom/abortcontroller.rs +++ b/components/script/dom/abortcontroller.rs @@ -3,24 +3,33 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use dom_struct::dom_struct; -use js::jsapi::Value; -use js::rust::{Handle, HandleObject}; +use js::rust::{HandleObject, HandleValue}; +use crate::dom::abortsignal::AbortSignal; use crate::dom::bindings::codegen::Bindings::AbortControllerBinding::AbortControllerMethods; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; -use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::{CanGc, JSContext}; +/// <https://dom.spec.whatwg.org/#abortcontroller> #[dom_struct] pub(crate) struct AbortController { reflector_: Reflector, + + /// An AbortController object has an associated signal (an AbortSignal object). + signal: Dom<AbortSignal>, } impl AbortController { + /// <https://dom.spec.whatwg.org/#dom-abortcontroller-abortcontroller> fn new_inherited() -> AbortController { + // The new AbortController() constructor steps are: + // Let signal be a new AbortSignal object. + // Set this’s signal to signal. AbortController { reflector_: Reflector::new(), + signal: Dom::from_ref(&AbortSignal::new_inherited()), } } @@ -36,6 +45,12 @@ impl AbortController { can_gc, ) } + + /// <https://dom.spec.whatwg.org/#abortcontroller-signal-abort> + fn signal_abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) { + // signal abort on controller’s signal with reason if it is given. + self.signal.signal_abort(cx, reason, can_gc); + } } impl AbortControllerMethods<crate::DomTypeHolder> for AbortController { @@ -49,5 +64,15 @@ impl AbortControllerMethods<crate::DomTypeHolder> for AbortController { } /// <https://dom.spec.whatwg.org/#dom-abortcontroller-abort> - fn Abort(&self, _cx: JSContext, _reason: Handle<'_, Value>) {} + fn Abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) { + // The abort(reason) method steps are + // to signal abort on this with reason if it is given. + self.signal_abort(cx, reason, can_gc); + } + + /// <https://dom.spec.whatwg.org/#dom-abortcontroller-signal> + fn Signal(&self) -> DomRoot<AbortSignal> { + // The signal getter steps are to return this’s signal. + self.signal.as_rooted() + } } diff --git a/components/script/dom/abortsignal.rs b/components/script/dom/abortsignal.rs index 57c6e9cd67e..e93a7b64e90 100644 --- a/components/script/dom/abortsignal.rs +++ b/components/script/dom/abortsignal.rs @@ -2,18 +2,37 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cell::RefCell; +use std::mem; + use dom_struct::dom_struct; -use js::jsapi::Heap; -use js::jsval::JSVal; -use js::rust::{HandleObject, MutableHandleValue}; +use js::jsapi::{ExceptionStackBehavior, Heap, JS_SetPendingException}; +use js::jsval::{JSVal, UndefinedValue}; +use js::rust::{HandleObject, HandleValue, MutableHandleValue}; +use script_bindings::inheritance::Castable; use crate::dom::bindings::codegen::Bindings::AbortSignalBinding::AbortSignalMethods; -use crate::dom::bindings::reflector::reflect_dom_object_with_proto; +use crate::dom::bindings::error::{Error, ErrorToJsval}; +use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto}; use crate::dom::bindings::root::DomRoot; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::{CanGc, JSContext}; +/// <https://dom.spec.whatwg.org/#abortcontroller-api-integration> +/// TODO: implement algorithms at call point, +/// in order to integrate the abort signal with its various use cases. +#[derive(JSTraceable, MallocSizeOf)] +#[allow(dead_code)] +enum AbortAlgorithm { + /// <https://dom.spec.whatwg.org/#add-an-event-listener> + DomEventLister, + /// <https://streams.spec.whatwg.org/#readable-stream-pipe-to> + StreamPiping, + /// <https://fetch.spec.whatwg.org/#dom-global-fetch> + Fetch, +} + /// <https://dom.spec.whatwg.org/#abortsignal> #[dom_struct] pub(crate) struct AbortSignal { @@ -22,14 +41,17 @@ pub(crate) struct AbortSignal { /// <https://dom.spec.whatwg.org/#abortsignal-abort-reason> #[ignore_malloc_size_of = "mozjs"] abort_reason: Heap<JSVal>, + + /// <https://dom.spec.whatwg.org/#abortsignal-abort-algorithms> + abort_algorithms: RefCell<Vec<AbortAlgorithm>>, } impl AbortSignal { - #[allow(dead_code)] - fn new_inherited() -> AbortSignal { + pub(crate) fn new_inherited() -> AbortSignal { AbortSignal { eventtarget: EventTarget::new_inherited(), abort_reason: Default::default(), + abort_algorithms: Default::default(), } } @@ -46,24 +68,88 @@ impl AbortSignal { can_gc, ) } + + /// <https://dom.spec.whatwg.org/#abortsignal-signal-abort> + pub(crate) fn signal_abort(&self, cx: JSContext, reason: HandleValue, can_gc: CanGc) { + // If signal is aborted, then return. + if self.Aborted() { + return; + } + + let abort_reason = reason.get(); + + // Set signal’s abort reason to reason if it is given; + if !abort_reason.is_undefined() { + self.abort_reason.set(abort_reason); + } else { + // otherwise to a new "AbortError" DOMException. + rooted!(in(*cx) let mut rooted_error = UndefinedValue()); + Error::Abort.to_jsval(cx, &self.global(), rooted_error.handle_mut(), can_gc); + self.abort_reason.set(rooted_error.get()) + } + + // Let dependentSignalsToAbort be a new list. + // For each dependentSignal of signal’s dependent signals: + // TODO: #36936 + + // Run the abort steps for signal. + self.run_the_abort_steps(can_gc); + + // For each dependentSignal of dependentSignalsToAbort, run the abort steps for dependentSignal. + // TODO: #36936 + } + + /// <https://dom.spec.whatwg.org/#run-the-abort-steps> + fn run_the_abort_steps(&self, can_gc: CanGc) { + // For each algorithm of signal’s abort algorithms: run algorithm. + let algos = mem::take(&mut *self.abort_algorithms.borrow_mut()); + for _algo in algos { + // TODO: match on variant and implement algo steps. + // See the various items of #34866 + } + + // Empty signal’s abort algorithms. + // Done above with `take`. + + // Fire an event named abort at signal. + self.upcast::<EventTarget>() + .fire_event(atom!("abort"), can_gc); + } + + /// <https://dom.spec.whatwg.org/#abortsignal-aborted> + fn aborted(&self) -> bool { + // An AbortSignal object is aborted when its abort reason is not undefined. + !self.abort_reason.get().is_undefined() + } } impl AbortSignalMethods<crate::DomTypeHolder> for AbortSignal { /// <https://dom.spec.whatwg.org/#dom-abortsignal-aborted> fn Aborted(&self) -> bool { - // TODO - false + // The aborted getter steps are to return true if this is aborted; otherwise false. + self.aborted() } /// <https://dom.spec.whatwg.org/#dom-abortsignal-reason> - fn Reason(&self, _: JSContext, _rval: MutableHandleValue) { - // TODO + fn Reason(&self, _cx: JSContext, mut rval: MutableHandleValue) { + // The reason getter steps are to return this’s abort reason. + rval.set(self.abort_reason.get()); } /// <https://dom.spec.whatwg.org/#dom-abortsignal-throwifaborted> #[allow(unsafe_code)] fn ThrowIfAborted(&self) { - // TODO + // The throwIfAborted() method steps are to throw this’s abort reason, if this is aborted. + if self.aborted() { + let cx = GlobalScope::get_cx(); + unsafe { + JS_SetPendingException( + *cx, + self.abort_reason.handle(), + ExceptionStackBehavior::Capture, + ) + }; + } } // <https://dom.spec.whatwg.org/#dom-abortsignal-onabort> diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index dbf0f14ab68..1fe51407638 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -13,7 +13,7 @@ use std::str::FromStr; use std::{fmt, mem}; use content_security_policy as csp; -use cssparser::match_ignore_ascii_case; +use cssparser::{Parser as CssParser, ParserInput as CssParserInput, match_ignore_ascii_case}; use devtools_traits::AttrInfo; use dom_struct::dom_struct; use embedder_traits::InputMethodType; @@ -36,6 +36,8 @@ use style::applicable_declarations::ApplicableDeclarationBlock; use style::attr::{AttrValue, LengthOrPercentageOrAuto}; use style::context::QuirksMode; use style::invalidation::element::restyle_hints::RestyleHint; +use style::media_queries::MediaList; +use style::parser::ParserContext as CssParserContext; use style::properties::longhands::{ self, background_image, border_spacing, font_family, font_size, }; @@ -50,13 +52,14 @@ use style::selector_parser::{ }; use style::shared_lock::{Locked, SharedRwLock}; use style::stylesheets::layer_rule::LayerOrder; -use style::stylesheets::{CssRuleType, UrlExtraData}; +use style::stylesheets::{CssRuleType, Origin as CssOrigin, UrlExtraData}; use style::values::computed::Overflow; use style::values::generics::NonNegative; use style::values::generics::position::PreferredRatio; use style::values::generics::ratio::Ratio; use style::values::{AtomIdent, AtomString, CSSFloat, computed, specified}; use style::{ArcSlice, CaseSensitivityExt, dom_apis, thread_state}; +use style_traits::ParsingMode as CssParsingMode; use stylo_atoms::Atom; use stylo_dom::ElementState; use xml5ever::serialize::TraversalScope::{ @@ -118,7 +121,7 @@ use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlfieldsetelement::HTMLFieldSetElement; use crate::dom::htmlfontelement::{HTMLFontElement, HTMLFontElementLayoutHelpers}; use crate::dom::htmlformelement::FormControlElementHelpers; -use crate::dom::htmlhrelement::{HTMLHRElement, HTMLHRLayoutHelpers}; +use crate::dom::htmlhrelement::{HTMLHRElement, HTMLHRLayoutHelpers, SizePresentationalHint}; use crate::dom::htmliframeelement::{HTMLIFrameElement, HTMLIFrameElementLayoutMethods}; use crate::dom::htmlimageelement::{HTMLImageElement, LayoutHTMLImageElementHelpers}; use crate::dom::htmlinputelement::{HTMLInputElement, LayoutHTMLInputElementHelpers}; @@ -781,6 +784,33 @@ impl Element { .registered_intersection_observers .retain(|reg_obs| *reg_obs.observer != *observer) } + + /// <https://html.spec.whatwg.org/multipage/#matches-the-environment> + pub(crate) fn matches_environment(&self, media_query: &str) -> bool { + let document = self.owner_document(); + let quirks_mode = document.quirks_mode(); + let document_url_data = UrlExtraData(document.url().get_arc()); + // FIXME(emilio): This should do the same that we do for other media + // lists regarding the rule type and such, though it doesn't really + // matter right now... + // + // Also, ParsingMode::all() is wrong, and should be DEFAULT. + let context = CssParserContext::new( + CssOrigin::Author, + &document_url_data, + Some(CssRuleType::Style), + CssParsingMode::all(), + quirks_mode, + /* namespaces = */ Default::default(), + None, + None, + ); + let mut parser_input = CssParserInput::new(media_query); + let mut parser = CssParser::new(&mut parser_input); + let media_list = MediaList::parse(&context, &mut parser); + let result = media_list.evaluate(document.window().layout().device(), quirks_mode); + result + } } /// <https://dom.spec.whatwg.org/#valid-shadow-host-name> @@ -831,8 +861,14 @@ pub(crate) fn get_attr_for_layout<'dom>( pub(crate) trait LayoutElementHelpers<'dom> { fn attrs(self) -> &'dom [LayoutDom<'dom, Attr>]; - fn has_class_for_layout(self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool; + fn has_class_or_part_for_layout( + self, + name: &AtomIdent, + attr_name: &LocalName, + case_sensitivity: CaseSensitivity, + ) -> bool; fn get_classes_for_layout(self) -> Option<&'dom [Atom]>; + fn get_parts_for_layout(self) -> Option<&'dom [Atom]>; fn synthesize_presentational_hints_for_legacy_attributes<V>(self, hints: &mut V) where @@ -875,8 +911,13 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> { } #[inline] - fn has_class_for_layout(self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { - get_attr_for_layout(self, &ns!(), &local_name!("class")).is_some_and(|attr| { + fn has_class_or_part_for_layout( + self, + name: &AtomIdent, + attr_name: &LocalName, + case_sensitivity: CaseSensitivity, + ) -> bool { + get_attr_for_layout(self, &ns!(), attr_name).is_some_and(|attr| { attr.to_tokens() .unwrap() .iter() @@ -890,6 +931,11 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> { .map(|attr| attr.to_tokens().unwrap()) } + fn get_parts_for_layout(self) -> Option<&'dom [Atom]> { + get_attr_for_layout(self, &ns!(), &local_name!("part")) + .map(|attr| attr.to_tokens().unwrap()) + } + fn synthesize_presentational_hints_for_legacy_attributes<V>(self, hints: &mut V) where V: Push<ApplicableDeclarationBlock>, @@ -1276,6 +1322,47 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> { PropertyDeclaration::PaddingRight(cellpadding), )); } + + // https://html.spec.whatwg.org/multipage/#the-hr-element-2 + if let Some(size_info) = self + .downcast::<HTMLHRElement>() + .and_then(|hr_element| hr_element.get_size_info()) + { + match size_info { + SizePresentationalHint::SetHeightTo(height) => { + hints.push(from_declaration( + shared_lock, + PropertyDeclaration::Height(height), + )); + }, + SizePresentationalHint::SetAllBorderWidthValuesTo(border_width) => { + hints.push(from_declaration( + shared_lock, + PropertyDeclaration::BorderLeftWidth(border_width.clone()), + )); + hints.push(from_declaration( + shared_lock, + PropertyDeclaration::BorderRightWidth(border_width.clone()), + )); + hints.push(from_declaration( + shared_lock, + PropertyDeclaration::BorderTopWidth(border_width.clone()), + )); + hints.push(from_declaration( + shared_lock, + PropertyDeclaration::BorderBottomWidth(border_width), + )); + }, + SizePresentationalHint::SetBottomBorderWidthToZero => { + hints.push(from_declaration( + shared_lock, + PropertyDeclaration::BorderBottomWidth( + specified::border::BorderSideWidth::from_px(0.), + ), + )); + }, + } + } } fn get_span(self) -> Option<u32> { @@ -1924,6 +2011,16 @@ impl Element { }) } + pub(crate) fn is_part(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool { + self.get_attribute(&ns!(), &LocalName::from("part")) + .is_some_and(|attr| { + attr.value() + .as_tokens() + .iter() + .any(|atom| case_sensitivity.eq_atom(name, atom)) + }) + } + pub(crate) fn set_atomic_attribute( &self, local_name: &LocalName, @@ -3980,6 +4077,13 @@ impl ElementMethods<crate::DomTypeHolder> for Element { rooted!(in(*cx) let slottable = Slottable(Dom::from_ref(self.upcast::<Node>()))); slottable.find_a_slot(true) } + + /// <https://drafts.csswg.org/css-shadow-parts/#dom-element-part> + fn Part(&self) -> DomRoot<DOMTokenList> { + self.ensure_rare_data() + .part + .or_init(|| DOMTokenList::new(self, &local_name!("part"), None, CanGc::note())) + } } impl VirtualMethods for Element { @@ -4119,7 +4223,9 @@ impl VirtualMethods for Element { match *name { local_name!("id") => AttrValue::from_atomic(value.into()), local_name!("name") => AttrValue::from_atomic(value.into()), - local_name!("class") => AttrValue::from_serialized_tokenlist(value.into()), + local_name!("class") | local_name!("part") => { + AttrValue::from_serialized_tokenlist(value.into()) + }, _ => self .super_type() .unwrap() @@ -4430,7 +4536,9 @@ impl SelectorsElement for SelectorWrapper<'_> { // a string containing commas (separating each language tag in // a list) but the pseudo-class instead should be parsing and // storing separate <ident> or <string>s for each language tag. - NonTSPseudoClass::Lang(ref lang) => extended_filtering(&self.get_lang(), lang), + NonTSPseudoClass::Lang(ref lang) => { + extended_filtering(&self.upcast::<Node>().get_lang().unwrap_or_default(), lang) + }, NonTSPseudoClass::ReadOnly => { !Element::state(self).contains(NonTSPseudoClass::ReadWrite.state_flag()) @@ -4491,8 +4599,8 @@ impl SelectorsElement for SelectorWrapper<'_> { .is_some_and(|atom| case_sensitivity.eq_atom(id, atom)) } - fn is_part(&self, _name: &AtomIdent) -> bool { - false + fn is_part(&self, name: &AtomIdent) -> bool { + Element::is_part(self, name, CaseSensitivity::CaseSensitive) } fn imported_part(&self, _: &AtomIdent) -> Option<AtomIdent> { @@ -4770,24 +4878,7 @@ impl Element { } } - // https://html.spec.whatwg.org/multipage/#language - pub(crate) fn get_lang(&self) -> String { - self.upcast::<Node>() - .inclusive_ancestors(ShadowIncluding::Yes) - .filter_map(|node| { - node.downcast::<Element>().and_then(|el| { - el.get_attribute(&ns!(xml), &local_name!("lang")) - .or_else(|| el.get_attribute(&ns!(), &local_name!("lang"))) - .map(|attr| String::from(attr.Value())) - }) - // TODO: Check meta tags for a pragma-set default language - // TODO: Check HTTP Content-Language header - }) - .next() - .unwrap_or(String::new()) - } - - pub(crate) fn state(&self) -> ElementState { + pub fn state(&self) -> ElementState { self.state.get() } diff --git a/components/script/dom/event.rs b/components/script/dom/event.rs index 743ced42a4b..345038a08da 100644 --- a/components/script/dom/event.rs +++ b/components/script/dom/event.rs @@ -1124,7 +1124,13 @@ impl TaskOnce for EventTask { let target = self.target.root(); let bubbles = self.bubbles; let cancelable = self.cancelable; - target.fire_event_with_params(self.name, bubbles, cancelable, CanGc::note()); + target.fire_event_with_params( + self.name, + bubbles, + cancelable, + EventComposed::NotComposed, + CanGc::note(), + ); } } diff --git a/components/script/dom/eventsource.rs b/components/script/dom/eventsource.rs index 7cf7bd6106f..3e0070f91d7 100644 --- a/components/script/dom/eventsource.rs +++ b/components/script/dom/eventsource.rs @@ -61,6 +61,34 @@ enum ReadyState { Closed = 2, } +#[derive(JSTraceable, MallocSizeOf)] +struct DroppableEventSource { + canceller: DomRefCell<FetchCanceller>, +} + +impl DroppableEventSource { + pub(crate) fn new(canceller: DomRefCell<FetchCanceller>) -> Self { + DroppableEventSource { canceller } + } + + pub(crate) fn cancel(&self) { + self.canceller.borrow_mut().cancel(); + } + + pub(crate) fn set_canceller(&self, data: FetchCanceller) { + *self.canceller.borrow_mut() = data; + } +} + +// https://html.spec.whatwg.org/multipage/#garbage-collection-2 +impl Drop for DroppableEventSource { + fn drop(&mut self) { + // If an EventSource object is garbage collected while its connection is still open, + // the user agent must abort any instance of the fetch algorithm opened by this EventSource. + self.cancel(); + } +} + #[dom_struct] pub(crate) struct EventSource { eventtarget: EventTarget, @@ -74,7 +102,7 @@ pub(crate) struct EventSource { ready_state: Cell<ReadyState>, with_credentials: bool, - canceller: DomRefCell<FetchCanceller>, + droppable: DroppableEventSource, } enum ParserState { @@ -480,7 +508,7 @@ impl EventSource { ready_state: Cell::new(ReadyState::Connecting), with_credentials, - canceller: DomRefCell::new(Default::default()), + droppable: DroppableEventSource::new(DomRefCell::new(Default::default())), } } @@ -501,7 +529,7 @@ impl EventSource { // https://html.spec.whatwg.org/multipage/#sse-processing-model:fail-the-connection-3 pub(crate) fn cancel(&self) { - self.canceller.borrow_mut().cancel(); + self.droppable.cancel(); self.fail_the_connection(); } @@ -529,15 +557,6 @@ impl EventSource { } } -// https://html.spec.whatwg.org/multipage/#garbage-collection-2 -impl Drop for EventSource { - fn drop(&mut self) { - // If an EventSource object is garbage collected while its connection is still open, - // the user agent must abort any instance of the fetch algorithm opened by this EventSource. - self.canceller.borrow_mut().cancel(); - } -} - impl EventSourceMethods<crate::DomTypeHolder> for EventSource { // https://html.spec.whatwg.org/multipage/#dom-eventsource fn Constructor( @@ -632,7 +651,7 @@ impl EventSourceMethods<crate::DomTypeHolder> for EventSource { listener.notify_fetch(message.unwrap()); }), ); - *ev.canceller.borrow_mut() = FetchCanceller::new(request.id); + ev.droppable.set_canceller(FetchCanceller::new(request.id)); global .core_resource_thread() .send(CoreResourceMsg::Fetch( @@ -672,7 +691,7 @@ impl EventSourceMethods<crate::DomTypeHolder> for EventSource { fn Close(&self) { let GenerationId(prev_id) = self.generation_id.get(); self.generation_id.set(GenerationId(prev_id + 1)); - self.canceller.borrow_mut().cancel(); + self.droppable.cancel(); self.ready_state.set(ReadyState::Closed); } } diff --git a/components/script/dom/eventtarget.rs b/components/script/dom/eventtarget.rs index 1a5aafb0ae7..15f77f5fcd5 100644 --- a/components/script/dom/eventtarget.rs +++ b/components/script/dom/eventtarget.rs @@ -59,7 +59,7 @@ use crate::dom::bindings::trace::HashMapTracedValues; use crate::dom::document::Document; use crate::dom::element::Element; use crate::dom::errorevent::ErrorEvent; -use crate::dom::event::{Event, EventBubbles, EventCancelable, EventStatus}; +use crate::dom::event::{Event, EventBubbles, EventCancelable, EventComposed, EventStatus}; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlformelement::FormControlElementHelpers; use crate::dom::node::{Node, NodeTraits}; @@ -767,6 +767,7 @@ impl EventTarget { name, EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, + EventComposed::NotComposed, can_gc, ) } @@ -777,6 +778,7 @@ impl EventTarget { name, EventBubbles::Bubbles, EventCancelable::NotCancelable, + EventComposed::NotComposed, can_gc, ) } @@ -787,6 +789,7 @@ impl EventTarget { name, EventBubbles::DoesNotBubble, EventCancelable::Cancelable, + EventComposed::NotComposed, can_gc, ) } @@ -801,19 +804,22 @@ impl EventTarget { name, EventBubbles::Bubbles, EventCancelable::Cancelable, + EventComposed::NotComposed, can_gc, ) } - // https://dom.spec.whatwg.org/#concept-event-fire + /// <https://dom.spec.whatwg.org/#concept-event-fire> pub(crate) fn fire_event_with_params( &self, name: Atom, bubbles: EventBubbles, cancelable: EventCancelable, + composed: EventComposed, can_gc: CanGc, ) -> DomRoot<Event> { let event = Event::new(&self.global(), name, bubbles, cancelable, can_gc); + event.set_composed(composed.into()); event.fire(self, can_gc); event } diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index a34b49f4a65..ade7b86caa8 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -29,9 +29,10 @@ use crossbeam_channel::Sender; use devtools_traits::{PageError, ScriptToDevtoolsControlMsg}; use dom_struct::dom_struct; use embedder_traits::EmbedderMsg; +use euclid::default::Size2D; use http::HeaderMap; use hyper_serde::Serde; -use ipc_channel::ipc::{self, IpcSender}; +use ipc_channel::ipc::{self, IpcSender, IpcSharedMemory}; use ipc_channel::router::ROUTER; use js::glue::{IsWrapper, UnwrapObjectDynamic}; use js::jsapi::{ @@ -59,9 +60,11 @@ use net_traits::{ CoreResourceMsg, CoreResourceThread, FetchResponseListener, IpcSend, ReferrerPolicy, ResourceThreads, fetch_async, }; +use pixels::PixelFormat; use profile_traits::{ipc as profile_ipc, mem as profile_mem, time as profile_time}; use script_bindings::interfaces::GlobalScopeHelpers; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; +use snapshot::Snapshot; use timers::{TimerEventId, TimerEventRequest, TimerSource}; use url::Origin; use uuid::Uuid; @@ -2956,58 +2959,209 @@ impl GlobalScope { result == CheckResult::Blocked } + /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> + #[allow(clippy::too_many_arguments)] pub(crate) fn create_image_bitmap( &self, image: ImageBitmapSource, + _sx: i32, + _sy: i32, + sw: Option<i32>, + sh: Option<i32>, options: &ImageBitmapOptions, can_gc: CanGc, ) -> Rc<Promise> { let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>(); let p = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc); + + // Step 1. If either sw or sh is given and is 0, then return a promise rejected with a RangeError. + if sw.is_some_and(|w| w == 0) { + p.reject_error( + Error::Range("'sw' must be a non-zero value".to_owned()), + can_gc, + ); + return p; + } + + if sh.is_some_and(|h| h == 0) { + p.reject_error( + Error::Range("'sh' must be a non-zero value".to_owned()), + can_gc, + ); + return p; + } + + // Step 2. If either options's resizeWidth or options's resizeHeight is present and is 0, + // then return a promise rejected with an "InvalidStateError" DOMException. if options.resizeWidth.is_some_and(|w| w == 0) { p.reject_error(Error::InvalidState, can_gc); return p; } - if options.resizeHeight.is_some_and(|w| w == 0) { + if options.resizeHeight.is_some_and(|h| h == 0) { p.reject_error(Error::InvalidState, can_gc); return p; } + // Step 3. Check the usability of the image argument. If this throws an exception or returns bad, + // then return a promise rejected with an "InvalidStateError" DOMException. + // Step 6. Switch on image: match image { + ImageBitmapSource::HTMLImageElement(ref image) => { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if !image.is_usable().is_ok_and(|u| u) { + p.reject_error(Error::InvalidState, can_gc); + return p; + } + + // If no ImageBitmap object can be constructed, then the promise is rejected instead. + let Some(img) = image.image_data() else { + p.reject_error(Error::InvalidState, can_gc); + return p; + }; + + let Some(img) = img.as_raster_image() else { + // Vector HTMLImageElement are not yet supported. + p.reject_error(Error::InvalidState, can_gc); + return p; + }; + + let size = Size2D::new(img.metadata.width, img.metadata.height); + let format = match img.format { + PixelFormat::BGRA8 => snapshot::PixelFormat::BGRA, + PixelFormat::RGBA8 => snapshot::PixelFormat::RGBA, + pixel_format => { + unimplemented!("unsupported pixel format ({:?})", pixel_format) + }, + }; + let alpha_mode = snapshot::AlphaMode::Transparent { + premultiplied: false, + }; + + let snapshot = Snapshot::from_shared_memory( + size.cast(), + format, + alpha_mode, + IpcSharedMemory::from_bytes(img.first_frame().bytes), + ); + + let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); + image_bitmap.set_origin_clean(image.same_origin(GlobalScope::entry().origin())); + + p.resolve_native(&image_bitmap, can_gc); + }, + ImageBitmapSource::HTMLVideoElement(ref video) => { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if !video.is_usable() { + p.reject_error(Error::InvalidState, can_gc); + return p; + } + + if video.is_network_state_empty() { + p.reject_error(Error::InvalidState, can_gc); + return p; + } + + // If no ImageBitmap object can be constructed, then the promise is rejected instead. + let Some(snapshot) = video.get_current_frame_data() else { + p.reject_error(Error::InvalidState, can_gc); + return p; + }; + + let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); + image_bitmap.set_origin_clean(video.origin_is_clean()); + + p.resolve_native(&image_bitmap, can_gc); + }, ImageBitmapSource::HTMLCanvasElement(ref canvas) => { - // https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument - if !canvas.is_valid() { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if canvas.get_size().is_empty() { p.reject_error(Error::InvalidState, can_gc); return p; } - if let Some(snapshot) = canvas.get_image_data() { - let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); - image_bitmap.set_origin_clean(canvas.origin_is_clean()); - p.resolve_native(&(image_bitmap), can_gc); + // If no ImageBitmap object can be constructed, then the promise is rejected instead. + let Some(snapshot) = canvas.get_image_data() else { + p.reject_error(Error::InvalidState, can_gc); + return p; + }; + + let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); + image_bitmap.set_origin_clean(canvas.origin_is_clean()); + + p.resolve_native(&image_bitmap, can_gc); + }, + ImageBitmapSource::ImageBitmap(ref bitmap) => { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if bitmap.is_detached() { + p.reject_error(Error::InvalidState, can_gc); + return p; } - p + + // If no ImageBitmap object can be constructed, then the promise is rejected instead. + let Some(snapshot) = bitmap.bitmap_data().clone() else { + p.reject_error(Error::InvalidState, can_gc); + return p; + }; + + let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); + image_bitmap.set_origin_clean(bitmap.origin_is_clean()); + + p.resolve_native(&image_bitmap, can_gc); }, ImageBitmapSource::OffscreenCanvas(ref canvas) => { - // https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument - if !canvas.is_valid() { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if canvas.get_size().is_empty() { p.reject_error(Error::InvalidState, can_gc); return p; } - if let Some(snapshot) = canvas.get_image_data() { - let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); - image_bitmap.set_origin_clean(canvas.origin_is_clean()); - p.resolve_native(&(image_bitmap), can_gc); + // If no ImageBitmap object can be constructed, then the promise is rejected instead. + let Some(snapshot) = canvas.get_image_data() else { + p.reject_error(Error::InvalidState, can_gc); + return p; + }; + + let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); + image_bitmap.set_origin_clean(canvas.origin_is_clean()); + + p.resolve_native(&image_bitmap, can_gc); + }, + ImageBitmapSource::Blob(_) => { + // TODO: implement support of Blob object as ImageBitmapSource + p.reject_error(Error::InvalidState, can_gc); + }, + ImageBitmapSource::ImageData(ref image_data) => { + // <https://html.spec.whatwg.org/multipage/#the-imagebitmap-interface:imagedata-4> + if image_data.is_detached() { + p.reject_error(Error::InvalidState, can_gc); + return p; } - p + + let alpha_mode = snapshot::AlphaMode::Transparent { + premultiplied: false, + }; + + let snapshot = Snapshot::from_shared_memory( + image_data.get_size().cast(), + snapshot::PixelFormat::RGBA, + alpha_mode, + image_data.to_shared_memory(), + ); + + let image_bitmap = ImageBitmap::new(self, snapshot, can_gc); + + p.resolve_native(&image_bitmap, can_gc); }, - _ => { + ImageBitmapSource::CSSStyleValue(_) => { + // TODO: CSSStyleValue is not part of ImageBitmapSource + // <https://html.spec.whatwg.org/multipage/#imagebitmapsource> p.reject_error(Error::NotSupported, can_gc); - p }, } + + // Step 7. Return promise. + p } pub(crate) fn fire_timer(&self, handle: TimerEventId, can_gc: CanGc) { @@ -3495,7 +3649,8 @@ impl GlobalScope { Some(event_target) => Trusted::new(event_target.upcast()), }; // Step 3: Queue a task to run the following steps: - let task = CSPViolationReportTask::new(Trusted::new(self), target, report); + let task = + CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy); self.task_manager() .dom_manipulation_task_source() .queue(task); diff --git a/components/script/dom/htmlcanvaselement.rs b/components/script/dom/htmlcanvaselement.rs index 499e91c127b..9c688678039 100644 --- a/components/script/dom/htmlcanvaselement.rs +++ b/components/script/dom/htmlcanvaselement.rs @@ -16,7 +16,7 @@ use html5ever::{LocalName, Prefix, local_name, ns}; use image::codecs::jpeg::JpegEncoder; use image::codecs::png::PngEncoder; use image::codecs::webp::WebPEncoder; -use image::{ColorType, ImageEncoder}; +use image::{ColorType, ImageEncoder, ImageError}; #[cfg(feature = "webgpu")] use ipc_channel::ipc::{self as ipcchan}; use js::error::throw_type_error; @@ -362,7 +362,13 @@ impl HTMLCanvasElement { Some(context) => context.get_image_data(), None => { let size = self.get_size(); - if size.width == 0 || size.height == 0 { + if size.is_empty() || + pixels::compute_rgba8_byte_length_if_within_limit( + size.width as usize, + size.height as usize, + ) + .is_none() + { None } else { Some(Snapshot::cleared(size.cast())) @@ -385,22 +391,20 @@ impl HTMLCanvasElement { quality: Option<f64>, snapshot: &Snapshot, encoder: &mut W, - ) { + ) -> Result<(), ImageError> { // We can't use self.Width() or self.Height() here, since the size of the canvas // may have changed since the snapshot was created. Truncating the dimensions to a // u32 can't panic, since the data comes from a canvas which is always smaller than // u32::MAX. let canvas_data = snapshot.data(); - let width = snapshot.size().width as u32; - let height = snapshot.size().height as u32; + let width = snapshot.size().width; + let height = snapshot.size().height; match image_type { EncodedImageType::Png => { // FIXME(nox): https://github.com/image-rs/image-png/issues/86 // FIXME(nox): https://github.com/image-rs/image-png/issues/87 - PngEncoder::new(encoder) - .write_image(canvas_data, width, height, ColorType::Rgba8) - .unwrap(); + PngEncoder::new(encoder).write_image(canvas_data, width, height, ColorType::Rgba8) }, EncodedImageType::Jpeg => { let jpeg_encoder = if let Some(quality) = quality { @@ -418,16 +422,16 @@ impl HTMLCanvasElement { JpegEncoder::new(encoder) }; - jpeg_encoder - .write_image(canvas_data, width, height, ColorType::Rgba8) - .unwrap(); + jpeg_encoder.write_image(canvas_data, width, height, ColorType::Rgba8) }, - EncodedImageType::Webp => { // No quality support because of https://github.com/image-rs/image/issues/1984 - WebPEncoder::new_lossless(encoder) - .write_image(canvas_data, width, height, ColorType::Rgba8) - .unwrap(); + WebPEncoder::new_lossless(encoder).write_image( + canvas_data, + width, + height, + ColorType::Rgba8, + ) }, } } @@ -516,17 +520,22 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { mime_type: DOMString, quality: HandleValue, ) -> Fallible<USVString> { - // Step 1. + // Step 1: If this canvas element's bitmap's origin-clean flag is set to false, + // then throw a "SecurityError" DOMException. if !self.origin_is_clean() { return Err(Error::Security); } - // Step 2. + // Step 2: If this canvas element's bitmap has no pixels (i.e. either its + // horizontal dimension or its vertical dimension is zero), then return the string + // "data:,". (This is the shortest data: URL; it represents the empty string in a + // text/plain resource.) if self.Width() == 0 || self.Height() == 0 { return Ok(USVString("data:,".into())); } - // Step 3. + // Step 3: Let file be a serialization of this canvas element's bitmap as a file, + // passing type and quality if given. let Some(mut snapshot) = self.get_image_data() else { return Ok(USVString("data:,".into())); }; @@ -551,12 +560,20 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { &base64::engine::general_purpose::STANDARD, ); - self.encode_for_mime_type( - &image_type, - Self::maybe_quality(quality), - &snapshot, - &mut encoder, - ); + if self + .encode_for_mime_type( + &image_type, + Self::maybe_quality(quality), + &snapshot, + &mut encoder, + ) + .is_err() + { + // Step 4. If file is null, then return "data:,". + return Ok(USVString("data:,".into())); + } + + // Step 5. Return a data: URL representing file. [RFC2397] encoder.into_inner(); Ok(USVString(url)) } @@ -604,26 +621,37 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { return error!("Expected blob callback, but found none!"); }; - if let Some(mut snapshot) = result { - snapshot.transform( - snapshot::AlphaMode::Transparent{ premultiplied: false }, - snapshot::PixelFormat::RGBA - ); - // Step 4.1 - // If result is non-null, then set result to a serialization of result as a file with - // type and quality if given. - let mut encoded: Vec<u8> = vec![]; - - this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded); - let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); - // Step 4.2.1 Set result to a new Blob object, created in the relevant realm of this canvas element - let blob = Blob::new(&this.global(), blob_impl, CanGc::note()); - - // Step 4.2.2 Invoke callback with « result » and "report". - let _ = callback.Call__(Some(&blob), ExceptionHandling::Report, CanGc::note()); - } else { + let Some(mut snapshot) = result else { let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note()); - } + return; + }; + + snapshot.transform( + snapshot::AlphaMode::Transparent{ premultiplied: false }, + snapshot::PixelFormat::RGBA + ); + + // Step 4.1: If result is non-null, then set result to a serialization of + // result as a file with type and quality if given. + // Step 4.2: Queue an element task on the canvas blob serialization task + // source given the canvas element to run these steps: + let mut encoded: Vec<u8> = vec![]; + let blob_impl; + let blob; + let result = match this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded) { + Ok(..) => { + // Step 4.2.1: If result is non-null, then set result to a new Blob + // object, created in the relevant realm of this canvas element, + // representing result. [FILEAPI] + blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); + blob = Blob::new(&this.global(), blob_impl, CanGc::note()); + Some(&*blob) + } + Err(..) => None, + }; + + // Step 4.2.2: Invoke callback with « result » and "report". + let _ = callback.Call__(result, ExceptionHandling::Report, CanGc::note()); })); Ok(()) diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs index f41370386e9..f47a40d3cdb 100644 --- a/components/script/dom/htmlelement.rs +++ b/components/script/dom/htmlelement.rs @@ -437,7 +437,7 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement { document.request_focus(None, FocusInitiator::Local, can_gc); } - // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent + /// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent> fn GetOffsetParent(&self, can_gc: CanGc) -> Option<DomRoot<Element>> { if self.is::<HTMLBodyElement>() || self.is::<HTMLHtmlElement>() { return None; diff --git a/components/script/dom/htmlhrelement.rs b/components/script/dom/htmlhrelement.rs index c88a0fcf184..8dc11e4e848 100644 --- a/components/script/dom/htmlhrelement.rs +++ b/components/script/dom/htmlhrelement.rs @@ -2,11 +2,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::str::FromStr; + use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; use style::attr::{AttrValue, LengthOrPercentageOrAuto}; use style::color::AbsoluteColor; +use style::values::generics::NonNegative; +use style::values::specified::border::BorderSideWidth; +use style::values::specified::length::Size; +use style::values::specified::{LengthPercentage, NoCalcLength}; use crate::dom::bindings::codegen::Bindings::HTMLHRElementBinding::HTMLHRElementMethods; use crate::dom::bindings::inheritance::Castable; @@ -65,6 +71,18 @@ impl HTMLHRElementMethods<crate::DomTypeHolder> for HTMLHRElement { // https://html.spec.whatwg.org/multipage/#dom-hr-color make_legacy_color_setter!(SetColor, "color"); + // https://html.spec.whatwg.org/multipage/#dom-hr-noshade + make_bool_getter!(NoShade, "noshade"); + + // https://html.spec.whatwg.org/multipage/#dom-hr-noshade + make_bool_setter!(SetNoShade, "noshade"); + + // https://html.spec.whatwg.org/multipage/#dom-hr-size + make_getter!(Size, "size"); + + // https://html.spec.whatwg.org/multipage/#dom-hr-size + make_dimension_setter!(SetSize, "size"); + // https://html.spec.whatwg.org/multipage/#dom-hr-width make_getter!(Width, "width"); @@ -72,9 +90,20 @@ impl HTMLHRElementMethods<crate::DomTypeHolder> for HTMLHRElement { make_dimension_setter!(SetWidth, "width"); } +/// The result of applying the the presentational hint for the `size` attribute. +/// +/// (This attribute can mean different things depending on its value and other attributes) +#[allow(clippy::enum_variant_names)] +pub(crate) enum SizePresentationalHint { + SetHeightTo(Size), + SetAllBorderWidthValuesTo(BorderSideWidth), + SetBottomBorderWidthToZero, +} + pub(crate) trait HTMLHRLayoutHelpers { fn get_color(self) -> Option<AbsoluteColor>; fn get_width(self) -> LengthOrPercentageOrAuto; + fn get_size_info(self) -> Option<SizePresentationalHint>; } impl HTMLHRLayoutHelpers for LayoutDom<'_, HTMLHRElement> { @@ -92,6 +121,35 @@ impl HTMLHRLayoutHelpers for LayoutDom<'_, HTMLHRElement> { .cloned() .unwrap_or(LengthOrPercentageOrAuto::Auto) } + + fn get_size_info(self) -> Option<SizePresentationalHint> { + // https://html.spec.whatwg.org/multipage/#the-hr-element-2 + let element = self.upcast::<Element>(); + let size_value = element + .get_attr_val_for_layout(&ns!(), &local_name!("size")) + .and_then(|value| usize::from_str(value).ok()) + .filter(|value| *value != 0)?; + + let hint = if element + .get_attr_for_layout(&ns!(), &local_name!("color")) + .is_some() || + element + .get_attr_for_layout(&ns!(), &local_name!("noshade")) + .is_some() + { + SizePresentationalHint::SetAllBorderWidthValuesTo(BorderSideWidth::from_px( + size_value as f32 / 2.0, + )) + } else if size_value == 1 { + SizePresentationalHint::SetBottomBorderWidthToZero + } else { + SizePresentationalHint::SetHeightTo(Size::LengthPercentage(NonNegative( + LengthPercentage::Length(NoCalcLength::from_px((size_value - 2) as f32)), + ))) + }; + + Some(hint) + } } impl VirtualMethods for HTMLHRElement { diff --git a/components/script/dom/htmlimageelement.rs b/components/script/dom/htmlimageelement.rs index b9819000556..b1258c42cce 100644 --- a/components/script/dom/htmlimageelement.rs +++ b/components/script/dom/htmlimageelement.rs @@ -34,9 +34,8 @@ use servo_url::ServoUrl; use servo_url::origin::MutableOrigin; use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_integer, parse_length}; use style::context::QuirksMode; -use style::media_queries::MediaList; use style::parser::ParserContext; -use style::stylesheets::{CssRuleType, Origin, UrlExtraData}; +use style::stylesheets::{CssRuleType, Origin}; use style::values::specified::AbsoluteLength; use style::values::specified::length::{Length, NoCalcLength}; use style::values::specified::source_size_list::SourceSizeList; @@ -678,7 +677,7 @@ impl HTMLImageElement { // Step 4.6 if let Some(x) = element.get_attribute(&ns!(), &local_name!("media")) { - if !self.matches_environment(x.value().to_string()) { + if !elem.matches_environment(&x.value()) { continue; } } @@ -722,33 +721,6 @@ impl HTMLImageElement { result } - /// <https://html.spec.whatwg.org/multipage/#matches-the-environment> - fn matches_environment(&self, media_query: String) -> bool { - let document = self.owner_document(); - let quirks_mode = document.quirks_mode(); - let document_url_data = UrlExtraData(document.url().get_arc()); - // FIXME(emilio): This should do the same that we do for other media - // lists regarding the rule type and such, though it doesn't really - // matter right now... - // - // Also, ParsingMode::all() is wrong, and should be DEFAULT. - let context = ParserContext::new( - Origin::Author, - &document_url_data, - Some(CssRuleType::Style), - ParsingMode::all(), - quirks_mode, - /* namespaces = */ Default::default(), - None, - None, - ); - let mut parserInput = ParserInput::new(&media_query); - let mut parser = Parser::new(&mut parserInput); - let media_list = MediaList::parse(&context, &mut parser); - let result = media_list.evaluate(document.window().layout().device(), quirks_mode); - result - } - /// <https://html.spec.whatwg.org/multipage/#normalise-the-source-densities> fn normalise_source_densities(&self, source_set: &mut SourceSet, width: Option<Length>) { // Step 1 diff --git a/components/script/dom/htmllinkelement.rs b/components/script/dom/htmllinkelement.rs index f4e7683cf2a..41edb2c22d3 100644 --- a/components/script/dom/htmllinkelement.rs +++ b/components/script/dom/htmllinkelement.rs @@ -5,6 +5,7 @@ use std::borrow::{Borrow, ToOwned}; use std::cell::Cell; use std::default::Default; +use std::str::FromStr; use base::id::WebViewId; use content_security_policy as csp; @@ -12,6 +13,8 @@ use dom_struct::dom_struct; use embedder_traits::EmbedderMsg; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; +use mime::Mime; +use net_traits::mime_classifier::{MediaType, MimeClassifier}; use net_traits::policy_container::PolicyContainer; use net_traits::request::{ CorsSettings, Destination, Initiator, InsecureRequestsPolicy, Referrer, RequestBuilder, @@ -22,7 +25,7 @@ use net_traits::{ ResourceTimingType, }; use servo_arc::Arc; -use servo_url::ServoUrl; +use servo_url::{ImmutableOrigin, ServoUrl}; use style::attr::AttrValue; use style::stylesheets::Stylesheet; use stylo_atoms::Atom; @@ -78,6 +81,7 @@ struct LinkProcessingOptions { policy_container: PolicyContainer, source_set: Option<()>, base_url: ServoUrl, + origin: ImmutableOrigin, insecure_requests_policy: InsecureRequestsPolicy, has_trustworthy_ancestor_origin: bool, // Some fields that we don't need yet are missing @@ -113,6 +117,10 @@ pub(crate) struct HTMLLinkElement { request_generation_id: Cell<RequestGenerationId>, /// <https://html.spec.whatwg.org/multipage/#explicitly-enabled> is_explicitly_enabled: Cell<bool>, + /// Whether the previous type matched with the destination + previous_type_matched: Cell<bool>, + /// Whether the previous media environment matched with the media query + previous_media_environment_matched: Cell<bool>, } impl HTMLLinkElement { @@ -133,6 +141,8 @@ impl HTMLLinkElement { any_failed_load: Cell::new(false), request_generation_id: Cell::new(RequestGenerationId(0)), is_explicitly_enabled: Cell::new(false), + previous_type_matched: Cell::new(true), + previous_media_environment_matched: Cell::new(true), } } @@ -236,7 +246,7 @@ impl VirtualMethods for HTMLLinkElement { return; } - if !self.upcast::<Node>().is_connected() || is_removal { + if !self.upcast::<Node>().is_connected() { return; } match *local_name { @@ -245,6 +255,12 @@ impl VirtualMethods for HTMLLinkElement { .set(LinkRelations::for_element(self.upcast())); }, local_name!("href") => { + if is_removal { + return; + } + // https://html.spec.whatwg.org/multipage/#link-type-stylesheet + // When the href attribute of the link element of an external resource link + // that is already browsing-context connected is changed. if self.relations.get().contains(LinkRelations::STYLESHEET) { self.handle_stylesheet_url(&attr.value()); } @@ -254,9 +270,19 @@ impl VirtualMethods for HTMLLinkElement { self.handle_favicon_url(&attr.value(), &sizes); } + // https://html.spec.whatwg.org/multipage/#link-type-prefetch + // When the href attribute of the link element of an external resource link + // that is already browsing-context connected is changed. if self.relations.get().contains(LinkRelations::PREFETCH) { self.fetch_and_process_prefetch_link(&attr.value()); } + + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the href attribute of the link element of an external resource link + // that is already browsing-context connected is changed. + if self.relations.get().contains(LinkRelations::PRELOAD) { + self.handle_preload_url(); + } }, local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => { if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) { @@ -264,9 +290,73 @@ impl VirtualMethods for HTMLLinkElement { } }, local_name!("crossorigin") => { + // https://html.spec.whatwg.org/multipage/#link-type-prefetch + // When the crossorigin attribute of the link element of an external resource link + // that is already browsing-context connected is set, changed, or removed. if self.relations.get().contains(LinkRelations::PREFETCH) { self.fetch_and_process_prefetch_link(&attr.value()); } + + // https://html.spec.whatwg.org/multipage/#link-type-stylesheet + // When the crossorigin attribute of the link element of an external resource link + // that is already browsing-context connected is set, changed, or removed. + if self.relations.get().contains(LinkRelations::STYLESHEET) { + self.handle_stylesheet_url(&attr.value()); + } + }, + local_name!("as") => { + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the as attribute of the link element of an external resource link + // that is already browsing-context connected is changed. + if self.relations.get().contains(LinkRelations::PRELOAD) { + if let AttributeMutation::Set(Some(_)) = mutation { + self.handle_preload_url(); + } + } + }, + local_name!("type") => { + // https://html.spec.whatwg.org/multipage/#link-type-stylesheet + // When the type attribute of the link element of an external resource link that + // is already browsing-context connected is set or changed to a value that does + // not or no longer matches the Content-Type metadata of the previous obtained + // external resource, if any. + // + // TODO: Match Content-Type metadata to check if it needs to be updated + if self.relations.get().contains(LinkRelations::STYLESHEET) { + self.handle_stylesheet_url(&attr.value()); + } + + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the type attribute of the link element of an external resource link that + // is already browsing-context connected, but was previously not obtained due to + // the type attribute specifying an unsupported type for the request destination, + // is set, removed, or changed. + if self.relations.get().contains(LinkRelations::PRELOAD) && + !self.previous_type_matched.get() + { + self.handle_preload_url(); + } + }, + local_name!("media") => { + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the media attribute of the link element of an external resource link that + // is already browsing-context connected, but was previously not obtained due to + // the media attribute not matching the environment, is changed or removed. + if self.relations.get().contains(LinkRelations::PRELOAD) && + !self.previous_media_environment_matched.get() + { + match mutation { + AttributeMutation::Removed | AttributeMutation::Set(Some(_)) => { + self.handle_preload_url() + }, + _ => {}, + }; + } + + let matches_media_environment = + self.upcast::<Element>().matches_environment(&attr.value()); + self.previous_media_environment_matched + .set(matches_media_environment); }, _ => {}, } @@ -307,6 +397,10 @@ impl VirtualMethods for HTMLLinkElement { if relations.contains(LinkRelations::PREFETCH) { self.fetch_and_process_prefetch_link(&href); } + + if relations.contains(LinkRelations::PRELOAD) { + self.handle_preload_url(); + } } } } @@ -325,6 +419,14 @@ impl VirtualMethods for HTMLLinkElement { } impl HTMLLinkElement { + fn compute_destination_for_attribute(&self) -> Destination { + let element = self.upcast::<Element>(); + element + .get_attribute(&ns!(), &local_name!("as")) + .map(|attr| translate_a_preload_destination(&attr.value())) + .unwrap_or(Destination::None) + } + /// <https://html.spec.whatwg.org/multipage/#create-link-options-from-element> fn processing_options(&self) -> LinkProcessingOptions { let element = self.upcast::<Element>(); @@ -333,10 +435,7 @@ impl HTMLLinkElement { let document = self.upcast::<Node>().owner_doc(); // Step 2. Let options be a new link processing options - let destination = element - .get_attribute(&ns!(), &local_name!("as")) - .map(|attr| translate_a_preload_destination(&attr.value())) - .unwrap_or(Destination::None); + let destination = self.compute_destination_for_attribute(); let mut options = LinkProcessingOptions { href: String::new(), @@ -348,6 +447,7 @@ impl HTMLLinkElement { referrer_policy: referrer_policy_for_element(element), policy_container: document.policy_container().to_owned(), source_set: None, // FIXME + origin: document.borrow().origin().immutable().to_owned(), base_url: document.borrow().base_url(), insecure_requests_policy: document.insecure_requests_policy(), has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(), @@ -446,6 +546,10 @@ impl HTMLLinkElement { None => "", }; + if !element.matches_environment(mq_str) { + return; + } + let media = MediaList::parse_media_list(mq_str, document.window()); let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity")); @@ -458,8 +562,6 @@ impl HTMLLinkElement { self.request_generation_id .set(self.request_generation_id.get().increment()); - // TODO: #8085 - Don't load external stylesheets if the node's mq - // doesn't match. let loader = StylesheetLoader::for_element(self.upcast()); loader.load( StylesheetContextSource::LinkElement { media: Some(media) }, @@ -494,6 +596,133 @@ impl HTMLLinkElement { Err(e) => debug!("Parsing url {} failed: {}", href, e), } } + + /// <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2> + fn handle_preload_url(&self) { + // Step 1. Update the source set for el. + // TODO + // Step 2. Let options be the result of creating link options from el. + let options = self.processing_options(); + // Step 3. Preload options, with the following steps given a response response: + // Step 3.1 If response is a network error, fire an event named error at el. + // Otherwise, fire an event named load at el. + self.preload(options); + } + + /// <https://html.spec.whatwg.org/multipage/#preload> + fn preload(&self, options: LinkProcessingOptions) { + // Step 1. If options's type doesn't match options's destination, then return. + let type_matches_destination: bool = + HTMLLinkElement::type_matches_destination(&options.link_type, options.destination); + self.previous_type_matched.set(type_matches_destination); + if !type_matches_destination { + return; + } + // Step 2. If options's destination is "image" and options's source set is not null, + // then set options's href to the result of selecting an image source from options's source set. + // TODO + // Step 3. Let request be the result of creating a link request given options. + let url = options.base_url.clone(); + let Some(request) = options.create_link_request(self.owner_window().webview_id()) else { + // Step 4. If request is null, then return. + return; + }; + let document = self.upcast::<Node>().owner_doc(); + // Step 5. Let unsafeEndTime be 0. + // TODO + // Step 6. Let entry be a new preload entry whose integrity metadata is options's integrity. + // TODO + // Step 7. Let key be the result of creating a preload key given request. + // TODO + // Step 8. If options's document is "pending", then set request's initiator type to "early hint". + // TODO + // Step 9. Let controller be null. + // Step 10. Let reportTiming given a Document document be to report timing for controller + // given document's relevant global object. + // Step 11. Set controller to the result of fetching request, with processResponseConsumeBody + // set to the following steps given a response response and null, failure, or a byte sequence bodyBytes: + let fetch_context = PreloadContext { + url, + link: Trusted::new(self), + resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), + }; + document.fetch_background(request.clone(), fetch_context); + } + + /// <https://html.spec.whatwg.org/multipage/#match-preload-type> + fn type_matches_destination(mime_type: &str, destination: Option<Destination>) -> bool { + // Step 1. If type is an empty string, then return true. + if mime_type.is_empty() { + return true; + } + // Step 2. If destination is "fetch", then return true. + // + // Fetch is handled as an empty string destination in the spec: + // https://fetch.spec.whatwg.org/#concept-potential-destination-translate + let Some(destination) = destination else { + return false; + }; + if destination == Destination::None { + return true; + } + // Step 3. Let mimeTypeRecord be the result of parsing type. + let Ok(mime_type_record) = Mime::from_str(mime_type) else { + // Step 4. If mimeTypeRecord is failure, then return false. + return false; + }; + // Step 5. If mimeTypeRecord is not supported by the user agent, then return false. + // + // We currently don't check if we actually support the mime type. Only if we can classify + // it according to the spec. + let Some(mime_type) = MimeClassifier::get_media_type(&mime_type_record) else { + return false; + }; + // Step 6. If any of the following are true: + if + // destination is "audio" or "video", and mimeTypeRecord is an audio or video MIME type; + ((destination == Destination::Audio || destination == Destination::Video) && + mime_type == MediaType::AudioVideo) + // destination is a script-like destination and mimeTypeRecord is a JavaScript MIME type; + || (destination.is_script_like() && mime_type == MediaType::JavaScript) + // destination is "image" and mimeTypeRecord is an image MIME type; + || (destination == Destination::Image && mime_type == MediaType::Image) + // destination is "font" and mimeTypeRecord is a font MIME type; + || (destination == Destination::Font && mime_type == MediaType::Font) + // destination is "json" and mimeTypeRecord is a JSON MIME type; + || (destination == Destination::Json && mime_type == MediaType::Json) + // destination is "style" and mimeTypeRecord's essence is text/css; or + || (destination == Destination::Style && mime_type_record == mime::TEXT_CSS) + // destination is "track" and mimeTypeRecord's essence is text/vtt, + || (destination == Destination::Track && mime_type_record.essence_str() == "text/vtt") + { + // then return true. + return true; + } + // Step 7. Return false. + false + } + + fn fire_event_after_response(&self, response: Result<ResourceFetchTiming, NetworkError>) { + if response.is_err() { + self.upcast::<EventTarget>() + .fire_event(atom!("error"), CanGc::note()); + } else { + // TODO(35035): Figure out why we need to queue a task for the load event. Otherwise + // the performance timing data hasn't been saved yet, which fails several preload + // WPT tests that assume that performance timing information is available when + // the load event is fired. + let this = Trusted::new(self); + self.owner_global() + .task_manager() + .performance_timeline_task_source() + .queue(task!(preload_load_event: move || { + let this = this.root(); + this + .upcast::<EventTarget>() + .fire_event(atom!("load"), CanGc::note()); + })); + } + } } impl StylesheetOwner for HTMLLinkElement { @@ -552,6 +781,21 @@ impl HTMLLinkElementMethods<crate::DomTypeHolder> for HTMLLinkElement { .set_tokenlist_attribute(&local_name!("rel"), rel, can_gc); } + // https://html.spec.whatwg.org/multipage/#dom-link-as + make_enumerated_getter!( + As, + "as", + "fetch" | "audio" | "audioworklet" | "document" | "embed" | "font" | "frame" + | "iframe" | "image" | "json" | "manifest" | "object" | "paintworklet" + | "report" | "script" | "serviceworker" | "sharedworker" | "style" | "track" + | "video" | "webidentity" | "worker" | "xslt", + missing => "", + invalid => "" + ); + + // https://html.spec.whatwg.org/multipage/#dom-link-as + make_setter!(SetAs, "as"); + // https://html.spec.whatwg.org/multipage/#dom-link-media make_getter!(Media, "media"); @@ -689,6 +933,8 @@ impl LinkProcessingOptions { self.has_trustworthy_ancestor_origin, self.policy_container, ) + .initiator(Initiator::Link) + .origin(self.origin) .integrity_metadata(self.integrity) .cryptographic_nonce_metadata(self.cryptographic_nonce_metadata) .referrer_policy(self.referrer_policy); @@ -795,3 +1041,77 @@ impl PreInvoke for PrefetchContext { true } } + +struct PreloadContext { + /// The `<link>` element that caused this preload operation + link: Trusted<HTMLLinkElement>, + + resource_timing: ResourceFetchTiming, + + /// The url being preloaded + url: ServoUrl, +} + +impl FetchResponseListener for PreloadContext { + fn process_request_body(&mut self, _: RequestId) {} + + fn process_request_eof(&mut self, _: RequestId) {} + + fn process_response( + &mut self, + _: RequestId, + fetch_metadata: Result<FetchMetadata, NetworkError>, + ) { + _ = fetch_metadata; + } + + fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) { + _ = chunk; + } + + /// Step 3.1 of <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2> + fn process_response_eof( + &mut self, + _: RequestId, + response: Result<ResourceFetchTiming, NetworkError>, + ) { + self.link.root().fire_event_after_response(response); + } + + fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming { + &mut self.resource_timing + } + + fn resource_timing(&self) -> &ResourceFetchTiming { + &self.resource_timing + } + + fn submit_resource_timing(&mut self) { + submit_timing(self, CanGc::note()) + } + + fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<csp::Violation>) { + let global = &self.resource_timing_global(); + global.report_csp_violations(violations, None); + } +} + +impl ResourceTimingListener for PreloadContext { + fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { + ( + InitiatorType::LocalName(self.url.clone().into_string()), + self.url.clone(), + ) + } + + fn resource_timing_global(&self) -> DomRoot<GlobalScope> { + self.link.root().upcast::<Node>().owner_doc().global() + } +} + +impl PreInvoke for PreloadContext { + fn should_invoke(&self) -> bool { + // Preload requests are never aborted. + true + } +} diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index e2b31e918de..e483504cceb 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -330,6 +330,34 @@ impl From<MediaStreamOrBlob> for SrcObject { } } +#[derive(JSTraceable, MallocSizeOf)] +struct DroppableHtmlMediaElement { + /// Player Id reported the player thread + player_id: Cell<u64>, + #[ignore_malloc_size_of = "Defined in other crates"] + #[no_trace] + player_context: WindowGLContext, +} + +impl DroppableHtmlMediaElement { + fn new(player_id: Cell<u64>, player_context: WindowGLContext) -> Self { + Self { + player_id, + player_context, + } + } + + pub(crate) fn set_player_id(&self, id: u64) { + self.player_id.set(id); + } +} + +impl Drop for DroppableHtmlMediaElement { + fn drop(&mut self) { + self.player_context + .send(GLPlayerMsg::UnregisterPlayer(self.player_id.get())); + } +} #[dom_struct] #[allow(non_snake_case)] pub(crate) struct HTMLMediaElement { @@ -411,16 +439,12 @@ pub(crate) struct HTMLMediaElement { next_timeupdate_event: Cell<Instant>, /// Latest fetch request context. current_fetch_context: DomRefCell<Option<HTMLMediaElementFetchContext>>, - /// Player Id reported the player thread - id: Cell<u64>, /// Media controls id. /// In order to workaround the lack of privileged JS context, we secure the /// the access to the "privileged" document.servoGetMediaControls(id) API by /// keeping a whitelist of media controls identifiers. media_controls_id: DomRefCell<Option<String>>, - #[ignore_malloc_size_of = "Defined in other crates"] - #[no_trace] - player_context: WindowGLContext, + droppable: DroppableHtmlMediaElement, } /// <https://html.spec.whatwg.org/multipage/#dom-media-networkstate> @@ -488,12 +512,18 @@ impl HTMLMediaElement { text_tracks_list: Default::default(), next_timeupdate_event: Cell::new(Instant::now() + Duration::from_millis(250)), current_fetch_context: DomRefCell::new(None), - id: Cell::new(0), media_controls_id: DomRefCell::new(None), - player_context: document.window().get_player_context(), + droppable: DroppableHtmlMediaElement::new( + Cell::new(0), + document.window().get_player_context(), + ), } } + pub(crate) fn network_state(&self) -> NetworkState { + self.network_state.get() + } + pub(crate) fn get_ready_state(&self) -> ReadyState { self.ready_state.get() } @@ -878,6 +908,9 @@ impl HTMLMediaElement { fn fetch_request(&self, offset: Option<u64>, seek_lock: Option<SeekLock>) { if self.resource_url.borrow().is_none() && self.blob_url.borrow().is_none() { eprintln!("Missing request url"); + if let Some(seek_lock) = seek_lock { + seek_lock.unlock(/* successful seek */ false); + } self.queue_dedicated_media_source_failure_steps(); return; } @@ -923,9 +956,17 @@ impl HTMLMediaElement { *current_fetch_context = Some(HTMLMediaElementFetchContext::new(request.id)); let listener = - HTMLMediaElementFetchListener::new(self, url.clone(), offset.unwrap_or(0), seek_lock); + HTMLMediaElementFetchListener::new(self, request.id, url.clone(), offset.unwrap_or(0)); self.owner_document().fetch_background(request, listener); + + // Since we cancelled the previous fetch, from now on the media element + // will only receive response data from the new fetch that's been + // initiated. This means the player can resume operation, since all subsequent data + // pushes will originate from the new seek offset. + if let Some(seek_lock) = seek_lock { + seek_lock.unlock(/* successful seek */ true); + } } // https://html.spec.whatwg.org/multipage/#concept-media-load-resource @@ -1357,6 +1398,10 @@ impl HTMLMediaElement { task_source.queue_simple_event(self.upcast(), atom!("seeked")); } + fn set_player_id(&self, player_id: u64) { + self.droppable.set_player_id(player_id); + } + /// <https://html.spec.whatwg.org/multipage/#poster-frame> pub(crate) fn process_poster_image_loaded(&self, image: Arc<RasterImage>) { if !self.show_poster.get() { @@ -1414,6 +1459,7 @@ impl HTMLMediaElement { audio_renderer, Box::new(window.get_player_context()), ); + let player_id = player.lock().unwrap().get_id(); *self.player.borrow_mut() = Some(player); @@ -1430,7 +1476,7 @@ impl HTMLMediaElement { trace!("Player event {:?}", event); let this = trusted_node.clone(); task_source.queue(task!(handle_player_event: move || { - this.root().handle_player_event(&event, CanGc::note()); + this.root().handle_player_event(player_id, &event, CanGc::note()); })); }), ); @@ -1451,7 +1497,7 @@ impl HTMLMediaElement { }) .unwrap_or((0, None)); - self.id.set(player_id); + self.set_player_id(player_id); self.video_renderer.lock().unwrap().player_id = Some(player_id); if let Some(image_receiver) = image_receiver { @@ -1514,406 +1560,458 @@ impl HTMLMediaElement { } } - fn handle_player_event(&self, event: &PlayerEvent, can_gc: CanGc) { - match *event { - PlayerEvent::EndOfStream => { - // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list - // => "If the media data can be fetched but is found by inspection to be in - // an unsupported format, or can otherwise not be rendered at all" - if self.ready_state.get() < ReadyState::HaveMetadata { - self.queue_dedicated_media_source_failure_steps(); - } else { - // https://html.spec.whatwg.org/multipage/#reaches-the-end - match self.direction_of_playback() { - PlaybackDirection::Forwards => { - // Step 1. - if self.Loop() { - self.seek( - self.earliest_possible_position(), - /* approximate_for_speed*/ false, - ); - } else { - // Step 2. - // The **ended playback** condition is implemented inside of - // the HTMLMediaElementMethods::Ended method - - // Step 3. - let this = Trusted::new(self); - - self.owner_global().task_manager().media_element_task_source().queue( - task!(reaches_the_end_steps: move || { - let this = this.root(); - // Step 3.1. - this.upcast::<EventTarget>().fire_event(atom!("timeupdate"), CanGc::note()); - - // Step 3.2. - if this.Ended() && !this.Paused() { - // Step 3.2.1. - this.paused.set(true); - - // Step 3.2.2. - this.upcast::<EventTarget>().fire_event(atom!("pause"), CanGc::note()); - - // Step 3.2.3. - this.take_pending_play_promises(Err(Error::Abort)); - this.fulfill_in_flight_play_promises(|| ()); - } - - // Step 3.3. - this.upcast::<EventTarget>().fire_event(atom!("ended"), CanGc::note()); - }) - ); - - // https://html.spec.whatwg.org/multipage/#dom-media-have_current_data - self.change_ready_state(ReadyState::HaveCurrentData); - } - }, + fn end_of_playback_in_forwards_direction(&self) { + // Step 1. If the media element has a loop attribute specified, then seek to the earliest + // posible position of the media resource and return. + if self.Loop() { + self.seek( + self.earliest_possible_position(), + /* approximate_for_speed*/ false, + ); + return; + } + // Step 2. The ended IDL attribute starts returning true once the event loop returns to + // step 1. + // The **ended playback** condition is implemented inside of + // the HTMLMediaElementMethods::Ended method - PlaybackDirection::Backwards => { - if self.playback_position.get() <= self.earliest_possible_position() { - self.owner_global() - .task_manager() - .media_element_task_source() - .queue_simple_event(self.upcast(), atom!("ended")); - } - }, - } - } - }, - PlayerEvent::Error(ref error) => { - error!("Player error: {:?}", error); + // Step 3. Queue a media element task given the media element and the following steps: + let this = Trusted::new(self); - // If we have already flagged an error condition while processing - // the network response, we should silently skip any observable - // errors originating while decoding the erroneous response. - if self.in_error_state() { - return; + self.owner_global() + .task_manager() + .media_element_task_source() + .queue(task!(reaches_the_end_steps: move || { + let this = this.root(); + // Step 3.1. Fire an event named timeupdate at the media element + this.upcast::<EventTarget>().fire_event(atom!("timeupdate"), CanGc::note()); + + // Step 3.2. If the media element has ended playback, the direction of playback is + // forwards, and paused is false, then: + if this.Ended() && !this.Paused() { + // Step 3.2.1. Set the paused attribute to true + this.paused.set(true); + + // Step 3.2.2. Fire an event named pause at the media element + this.upcast::<EventTarget>().fire_event(atom!("pause"), CanGc::note()); + + // Step 3.2.3. Take pending play promises and reject pending play promises with + // the result and an "AbortError" DOMException + this.take_pending_play_promises(Err(Error::Abort)); + this.fulfill_in_flight_play_promises(|| ()); } - // https://html.spec.whatwg.org/multipage/#loading-the-media-resource:media-data-13 - // 1. The user agent should cancel the fetching process. - if let Some(ref mut current_fetch_context) = - *self.current_fetch_context.borrow_mut() - { - current_fetch_context.cancel(CancelReason::Error); - } - // 2. Set the error attribute to the result of creating a MediaError with MEDIA_ERR_DECODE. - self.error.set(Some(&*MediaError::new( - &self.owner_window(), - MEDIA_ERR_DECODE, - can_gc, - ))); + // Step 3.3. Fire an event named ended at the media element. + this.upcast::<EventTarget>().fire_event(atom!("ended"), CanGc::note()); + })); - // 3. Set the element's networkState attribute to the NETWORK_IDLE value. - self.network_state.set(NetworkState::Idle); + // https://html.spec.whatwg.org/multipage/#dom-media-have_current_data + self.change_ready_state(ReadyState::HaveCurrentData); + } - // 4. Set the element's delaying-the-load-event flag to false. This stops delaying the load event. - self.delay_load_event(false, can_gc); + fn playback_end(&self) { + // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list + // => "If the media data can be fetched but is found by inspection to be in + // an unsupported format, or can otherwise not be rendered at all" + if self.ready_state.get() < ReadyState::HaveMetadata { + self.queue_dedicated_media_source_failure_steps(); + return; + } - // 5. Fire an event named error at the media element. - self.upcast::<EventTarget>() - .fire_event(atom!("error"), can_gc); + // https://html.spec.whatwg.org/multipage/#reaches-the-end + match self.direction_of_playback() { + PlaybackDirection::Forwards => self.end_of_playback_in_forwards_direction(), - // TODO: 6. Abort the overall resource selection algorithm. - }, - PlayerEvent::VideoFrameUpdated => { - // Check if the frame was resized - if let Some(frame) = self.video_renderer.lock().unwrap().current_frame { - self.handle_resize(Some(frame.width as u32), Some(frame.height as u32)); + PlaybackDirection::Backwards => { + if self.playback_position.get() <= self.earliest_possible_position() { + self.owner_global() + .task_manager() + .media_element_task_source() + .queue_simple_event(self.upcast(), atom!("ended")); } }, - PlayerEvent::MetadataUpdated(ref metadata) => { - // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list - // => If the media resource is found to have an audio track - if !metadata.audio_tracks.is_empty() { - for (i, _track) in metadata.audio_tracks.iter().enumerate() { - // Step 1. - let kind = match i { - 0 => DOMString::from("main"), - _ => DOMString::new(), - }; - let window = self.owner_window(); - let audio_track = AudioTrack::new( - &window, - DOMString::new(), - kind, - DOMString::new(), - DOMString::new(), - Some(&*self.AudioTracks()), - can_gc, - ); - - // Steps 2. & 3. - self.AudioTracks().add(&audio_track); - - // Step 4 - if let Some(servo_url) = self.resource_url.borrow().as_ref() { - let fragment = MediaFragmentParser::from(servo_url); - if let Some(id) = fragment.id() { - if audio_track.id() == DOMString::from(id) { - self.AudioTracks() - .set_enabled(self.AudioTracks().len() - 1, true); - } - } + } + } - if fragment.tracks().contains(&audio_track.kind().into()) { - self.AudioTracks() - .set_enabled(self.AudioTracks().len() - 1, true); - } - } + fn playback_error(&self, error: &str, can_gc: CanGc) { + error!("Player error: {:?}", error); + + // If we have already flagged an error condition while processing + // the network response, we should silently skip any observable + // errors originating while decoding the erroneous response. + if self.in_error_state() { + return; + } + + // https://html.spec.whatwg.org/multipage/#loading-the-media-resource:media-data-13 + // 1. The user agent should cancel the fetching process. + if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { + current_fetch_context.cancel(CancelReason::Error); + } + // 2. Set the error attribute to the result of creating a MediaError with MEDIA_ERR_DECODE. + self.error.set(Some(&*MediaError::new( + &self.owner_window(), + MEDIA_ERR_DECODE, + can_gc, + ))); + + // 3. Set the element's networkState attribute to the NETWORK_IDLE value. + self.network_state.set(NetworkState::Idle); + + // 4. Set the element's delaying-the-load-event flag to false. This stops delaying the load event. + self.delay_load_event(false, can_gc); + + // 5. Fire an event named error at the media element. + self.upcast::<EventTarget>() + .fire_event(atom!("error"), can_gc); + + // TODO: 6. Abort the overall resource selection algorithm. + } - // Step 5. & 6, - if self.AudioTracks().enabled_index().is_none() { + fn playback_metadata_updated( + &self, + metadata: &servo_media::player::metadata::Metadata, + can_gc: CanGc, + ) { + // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list + // => If the media resource is found to have an audio track + if !metadata.audio_tracks.is_empty() { + for (i, _track) in metadata.audio_tracks.iter().enumerate() { + // Step 1. + let kind = match i { + 0 => DOMString::from("main"), + _ => DOMString::new(), + }; + let window = self.owner_window(); + let audio_track = AudioTrack::new( + &window, + DOMString::new(), + kind, + DOMString::new(), + DOMString::new(), + Some(&*self.AudioTracks()), + can_gc, + ); + + // Steps 2. & 3. + self.AudioTracks().add(&audio_track); + + // Step 4 + if let Some(servo_url) = self.resource_url.borrow().as_ref() { + let fragment = MediaFragmentParser::from(servo_url); + if let Some(id) = fragment.id() { + if audio_track.id() == DOMString::from(id) { self.AudioTracks() .set_enabled(self.AudioTracks().len() - 1, true); } + } - // Steps 7. - let event = TrackEvent::new( - self.global().as_window(), - atom!("addtrack"), - false, - false, - &Some(VideoTrackOrAudioTrackOrTextTrack::AudioTrack(audio_track)), - can_gc, - ); - - event - .upcast::<Event>() - .fire(self.upcast::<EventTarget>(), can_gc); + if fragment.tracks().contains(&audio_track.kind().into()) { + self.AudioTracks() + .set_enabled(self.AudioTracks().len() - 1, true); } } - // => If the media resource is found to have a video track - if !metadata.video_tracks.is_empty() { - for (i, _track) in metadata.video_tracks.iter().enumerate() { - // Step 1. - let kind = match i { - 0 => DOMString::from("main"), - _ => DOMString::new(), - }; - let window = self.owner_window(); - let video_track = VideoTrack::new( - &window, - DOMString::new(), - kind, - DOMString::new(), - DOMString::new(), - Some(&*self.VideoTracks()), - can_gc, - ); - - // Steps 2. & 3. - self.VideoTracks().add(&video_track); - - // Step 4. - if let Some(track) = self.VideoTracks().item(0) { - if let Some(servo_url) = self.resource_url.borrow().as_ref() { - let fragment = MediaFragmentParser::from(servo_url); - if let Some(id) = fragment.id() { - if track.id() == DOMString::from(id) { - self.VideoTracks().set_selected(0, true); - } - } else if fragment.tracks().contains(&track.kind().into()) { - self.VideoTracks().set_selected(0, true); - } - } - } + // Step 5. & 6, + if self.AudioTracks().enabled_index().is_none() { + self.AudioTracks() + .set_enabled(self.AudioTracks().len() - 1, true); + } - // Step 5. & 6. - if self.VideoTracks().selected_index().is_none() { - self.VideoTracks() - .set_selected(self.VideoTracks().len() - 1, true); - } + // Steps 7. + let event = TrackEvent::new( + self.global().as_window(), + atom!("addtrack"), + false, + false, + &Some(VideoTrackOrAudioTrackOrTextTrack::AudioTrack(audio_track)), + can_gc, + ); - // Steps 7. - let event = TrackEvent::new( - self.global().as_window(), - atom!("addtrack"), - false, - false, - &Some(VideoTrackOrAudioTrackOrTextTrack::VideoTrack(video_track)), - can_gc, - ); - - event - .upcast::<Event>() - .fire(self.upcast::<EventTarget>(), can_gc); - } - } + event + .upcast::<Event>() + .fire(self.upcast::<EventTarget>(), can_gc); + } + } - // => "Once enough of the media data has been fetched to determine the duration..." + // => If the media resource is found to have a video track + if !metadata.video_tracks.is_empty() { + for (i, _track) in metadata.video_tracks.iter().enumerate() { // Step 1. - // servo-media owns the media timeline. - - // Step 2. - // XXX(ferjm) Update the timeline offset. + let kind = match i { + 0 => DOMString::from("main"), + _ => DOMString::new(), + }; + let window = self.owner_window(); + let video_track = VideoTrack::new( + &window, + DOMString::new(), + kind, + DOMString::new(), + DOMString::new(), + Some(&*self.VideoTracks()), + can_gc, + ); - // Step 3. - self.playback_position.set(0.); + // Steps 2. & 3. + self.VideoTracks().add(&video_track); // Step 4. - let previous_duration = self.duration.get(); - if let Some(duration) = metadata.duration { - self.duration.set(duration.as_secs() as f64); - } else { - self.duration.set(f64::INFINITY); + if let Some(track) = self.VideoTracks().item(0) { + if let Some(servo_url) = self.resource_url.borrow().as_ref() { + let fragment = MediaFragmentParser::from(servo_url); + if let Some(id) = fragment.id() { + if track.id() == DOMString::from(id) { + self.VideoTracks().set_selected(0, true); + } + } else if fragment.tracks().contains(&track.kind().into()) { + self.VideoTracks().set_selected(0, true); + } + } } - if previous_duration != self.duration.get() { - self.owner_global() - .task_manager() - .media_element_task_source() - .queue_simple_event(self.upcast(), atom!("durationchange")); + + // Step 5. & 6. + if self.VideoTracks().selected_index().is_none() { + self.VideoTracks() + .set_selected(self.VideoTracks().len() - 1, true); } - // Step 5. - self.handle_resize(Some(metadata.width), Some(metadata.height)); + // Steps 7. + let event = TrackEvent::new( + self.global().as_window(), + atom!("addtrack"), + false, + false, + &Some(VideoTrackOrAudioTrackOrTextTrack::VideoTrack(video_track)), + can_gc, + ); - // Step 6. - self.change_ready_state(ReadyState::HaveMetadata); + event + .upcast::<Event>() + .fire(self.upcast::<EventTarget>(), can_gc); + } + } - // Step 7. - let mut jumped = false; - - // Step 8. - if self.default_playback_start_position.get() > 0. { - self.seek( - self.default_playback_start_position.get(), - /* approximate_for_speed*/ false, - ); - jumped = true; - } + // => "Once enough of the media data has been fetched to determine the duration..." + // Step 1. + // servo-media owns the media timeline. - // Step 9. - self.default_playback_start_position.set(0.); + // Step 2. + // XXX(ferjm) Update the timeline offset. - // Steps 10 and 11. - if let Some(servo_url) = self.resource_url.borrow().as_ref() { - let fragment = MediaFragmentParser::from(servo_url); - if let Some(start) = fragment.start() { - if start > 0. && start < self.duration.get() { - self.playback_position.set(start); - if !jumped { - self.seek(self.playback_position.get(), false) - } - } + // Step 3. + self.playback_position.set(0.); + + // Step 4. + let previous_duration = self.duration.get(); + if let Some(duration) = metadata.duration { + self.duration.set(duration.as_secs() as f64); + } else { + self.duration.set(f64::INFINITY); + } + if previous_duration != self.duration.get() { + self.owner_global() + .task_manager() + .media_element_task_source() + .queue_simple_event(self.upcast(), atom!("durationchange")); + } + + // Step 5. + self.handle_resize(Some(metadata.width), Some(metadata.height)); + + // Step 6. + self.change_ready_state(ReadyState::HaveMetadata); + + // Step 7. + let mut jumped = false; + + // Step 8. + if self.default_playback_start_position.get() > 0. { + self.seek( + self.default_playback_start_position.get(), + /* approximate_for_speed*/ false, + ); + jumped = true; + } + + // Step 9. + self.default_playback_start_position.set(0.); + + // Steps 10 and 11. + if let Some(servo_url) = self.resource_url.borrow().as_ref() { + let fragment = MediaFragmentParser::from(servo_url); + if let Some(start) = fragment.start() { + if start > 0. && start < self.duration.get() { + self.playback_position.set(start); + if !jumped { + self.seek(self.playback_position.get(), false) } } + } + } - // Step 12 & 13 are already handled by the earlier media track processing. + // Step 12 & 13 are already handled by the earlier media track processing. - // We wait until we have metadata to render the controls, so we render them - // with the appropriate size. - if self.Controls() { - self.render_controls(can_gc); - } + // We wait until we have metadata to render the controls, so we render them + // with the appropriate size. + if self.Controls() { + self.render_controls(can_gc); + } - let global = self.global(); - let window = global.as_window(); + let global = self.global(); + let window = global.as_window(); + + // Update the media session metadata title with the obtained metadata. + window.Navigator().MediaSession().update_title( + metadata + .title + .clone() + .unwrap_or(window.get_url().into_string()), + ); + } - // Update the media session metadata title with the obtained metadata. - window.Navigator().MediaSession().update_title( - metadata - .title - .clone() - .unwrap_or(window.get_url().into_string()), - ); - }, - PlayerEvent::NeedData => { - // The player needs more data. - // If we already have a valid fetch request, we do nothing. - // Otherwise, if we have no request and the previous request was - // cancelled because we got an EnoughData event, we restart - // fetching where we left. - if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() { - match current_fetch_context.cancel_reason() { - Some(reason) if *reason == CancelReason::Backoff => { - // XXX(ferjm) Ideally we should just create a fetch request from - // where we left. But keeping track of the exact next byte that the - // media backend expects is not the easiest task, so I'm simply - // seeking to the current playback position for now which will create - // a new fetch request for the last rendered frame. - self.seek(self.playback_position.get(), false) - }, - _ => (), - } + fn playback_video_frame_updated(&self) { + // Check if the frame was resized + if let Some(frame) = self.video_renderer.lock().unwrap().current_frame { + self.handle_resize(Some(frame.width as u32), Some(frame.height as u32)); + } + } + + fn playback_need_data(&self) { + // The player needs more data. + // If we already have a valid fetch request, we do nothing. + // Otherwise, if we have no request and the previous request was + // cancelled because we got an EnoughData event, we restart + // fetching where we left. + if let Some(ref current_fetch_context) = *self.current_fetch_context.borrow() { + if let Some(reason) = current_fetch_context.cancel_reason() { + // XXX(ferjm) Ideally we should just create a fetch request from + // where we left. But keeping track of the exact next byte that the + // media backend expects is not the easiest task, so I'm simply + // seeking to the current playback position for now which will create + // a new fetch request for the last rendered frame. + if *reason == CancelReason::Backoff { + self.seek(self.playback_position.get(), false); } - }, - PlayerEvent::EnoughData => { - self.change_ready_state(ReadyState::HaveEnoughData); - - // The player has enough data and it is asking us to stop pushing - // bytes, so we cancel the ongoing fetch request iff we are able - // to restart it from where we left. Otherwise, we continue the - // current fetch request, assuming that some frames will be dropped. - if let Some(ref mut current_fetch_context) = - *self.current_fetch_context.borrow_mut() - { - if current_fetch_context.is_seekable() { - current_fetch_context.cancel(CancelReason::Backoff); - } + return; + } + } + + if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { + if let Err(e) = { + let mut data_source = current_fetch_context.data_source().borrow_mut(); + data_source.set_locked(false); + data_source.process_into_player_from_queue(self.player.borrow().as_ref().unwrap()) + } { + // If we are pushing too much data and we know that we can + // restart the download later from where we left, we cancel + // the current request. Otherwise, we continue the request + // assuming that we may drop some frames. + if e == PlayerError::EnoughData { + current_fetch_context.cancel(CancelReason::Backoff); + } + } + } + } + + fn playback_enough_data(&self) { + self.change_ready_state(ReadyState::HaveEnoughData); + + // The player has enough data and it is asking us to stop pushing + // bytes, so we cancel the ongoing fetch request iff we are able + // to restart it from where we left. Otherwise, we continue the + // current fetch request, assuming that some frames will be dropped. + if let Some(ref mut current_fetch_context) = *self.current_fetch_context.borrow_mut() { + if current_fetch_context.is_seekable() { + current_fetch_context.cancel(CancelReason::Backoff); + } + } + } + + fn playback_position_changed(&self, position: u64) { + let position = position as f64; + let _ = self + .played + .borrow_mut() + .add(self.playback_position.get(), position); + self.playback_position.set(position); + self.time_marches_on(); + let media_position_state = + MediaPositionState::new(self.duration.get(), self.playbackRate.get(), position); + debug!( + "Sending media session event set position state {:?}", + media_position_state + ); + self.send_media_session_event(MediaSessionEvent::SetPositionState(media_position_state)); + } + + fn playback_seek_done(&self) { + // Continuation of + // https://html.spec.whatwg.org/multipage/#dom-media-seek + + // Step 13. + let task = MediaElementMicrotask::Seeked { + elem: DomRoot::from_ref(self), + generation_id: self.generation_id.get(), + }; + ScriptThread::await_stable_state(Microtask::MediaElement(task)); + } + + fn playback_state_changed(&self, state: &PlaybackState) { + let mut media_session_playback_state = MediaSessionPlaybackState::None_; + match *state { + PlaybackState::Paused => { + media_session_playback_state = MediaSessionPlaybackState::Paused; + if self.ready_state.get() == ReadyState::HaveMetadata { + self.change_ready_state(ReadyState::HaveEnoughData); } }, - PlayerEvent::PositionChanged(position) => { - let position = position as f64; - let _ = self - .played - .borrow_mut() - .add(self.playback_position.get(), position); - self.playback_position.set(position); - self.time_marches_on(); - let media_position_state = - MediaPositionState::new(self.duration.get(), self.playbackRate.get(), position); - debug!( - "Sending media session event set position state {:?}", - media_position_state - ); - self.send_media_session_event(MediaSessionEvent::SetPositionState( - media_position_state, - )); + PlaybackState::Playing => { + media_session_playback_state = MediaSessionPlaybackState::Playing; }, - PlayerEvent::SeekData(p, ref seek_lock) => { - self.fetch_request(Some(p), Some(seek_lock.clone())); + PlaybackState::Buffering => { + // Do not send the media session playback state change event + // in this case as a None_ state is expected to clean up the + // session. + return; }, - PlayerEvent::SeekDone(_) => { - // Continuation of - // https://html.spec.whatwg.org/multipage/#dom-media-seek - - // Step 13. - let task = MediaElementMicrotask::Seeked { - elem: DomRoot::from_ref(self), - generation_id: self.generation_id.get(), - }; - ScriptThread::await_stable_state(Microtask::MediaElement(task)); + _ => {}, + }; + debug!( + "Sending media session event playback state changed to {:?}", + media_session_playback_state + ); + self.send_media_session_event(MediaSessionEvent::PlaybackStateChange( + media_session_playback_state, + )); + } + + fn handle_player_event(&self, player_id: usize, event: &PlayerEvent, can_gc: CanGc) { + // Ignore the asynchronous event from previous player. + if self + .player + .borrow() + .as_ref() + .is_none_or(|player| player.lock().unwrap().get_id() != player_id) + { + return; + } + + match *event { + PlayerEvent::EndOfStream => self.playback_end(), + PlayerEvent::Error(ref error) => self.playback_error(error, can_gc), + PlayerEvent::VideoFrameUpdated => self.playback_video_frame_updated(), + PlayerEvent::MetadataUpdated(ref metadata) => { + self.playback_metadata_updated(metadata, can_gc) }, - PlayerEvent::StateChanged(ref state) => { - let mut media_session_playback_state = MediaSessionPlaybackState::None_; - match *state { - PlaybackState::Paused => { - media_session_playback_state = MediaSessionPlaybackState::Paused; - if self.ready_state.get() == ReadyState::HaveMetadata { - self.change_ready_state(ReadyState::HaveEnoughData); - } - }, - PlaybackState::Playing => { - media_session_playback_state = MediaSessionPlaybackState::Playing; - }, - PlaybackState::Buffering => { - // Do not send the media session playback state change event - // in this case as a None_ state is expected to clean up the - // session. - return; - }, - _ => {}, - }; - debug!( - "Sending media session event playback state changed to {:?}", - media_session_playback_state - ); - self.send_media_session_event(MediaSessionEvent::PlaybackStateChange( - media_session_playback_state, - )); + PlayerEvent::NeedData => self.playback_need_data(), + PlayerEvent::EnoughData => self.playback_enough_data(), + PlayerEvent::PositionChanged(position) => self.playback_position_changed(position), + PlayerEvent::SeekData(p, ref seek_lock) => { + self.fetch_request(Some(p), Some(seek_lock.clone())) }, + PlayerEvent::SeekDone(_) => self.playback_seek_done(), + PlayerEvent::StateChanged(ref state) => self.playback_state_changed(state), } } @@ -2111,13 +2209,6 @@ impl HTMLMediaElement { } } -impl Drop for HTMLMediaElement { - fn drop(&mut self) { - self.player_context - .send(GLPlayerMsg::UnregisterPlayer(self.id.get())); - } -} - impl HTMLMediaElementMethods<crate::DomTypeHolder> for HTMLMediaElement { // https://html.spec.whatwg.org/multipage/#dom-media-networkstate fn NetworkState(&self) -> u16 { @@ -2659,6 +2750,80 @@ enum Resource { Url(ServoUrl), } +#[derive(Debug, MallocSizeOf, PartialEq)] +enum DataBuffer { + Payload(Vec<u8>), + EndOfStream, +} + +#[derive(MallocSizeOf)] +struct BufferedDataSource { + /// During initial setup and seeking (including clearing the buffer queue + /// and resetting the end-of-stream state), the data source should be locked and + /// any request for processing should be ignored until the media player informs us + /// via the NeedData event that it is ready to accept incoming data. + locked: Cell<bool>, + /// Temporary storage for incoming data. + buffers: VecDeque<DataBuffer>, +} + +impl BufferedDataSource { + fn new() -> BufferedDataSource { + BufferedDataSource { + locked: Cell::new(true), + buffers: VecDeque::default(), + } + } + + fn set_locked(&self, locked: bool) { + self.locked.set(locked) + } + + fn add_buffer_to_queue(&mut self, buffer: DataBuffer) { + debug_assert_ne!( + self.buffers.back(), + Some(&DataBuffer::EndOfStream), + "The media backend not expects any further data after end of stream" + ); + + self.buffers.push_back(buffer); + } + + fn process_into_player_from_queue( + &mut self, + player: &Arc<Mutex<dyn Player>>, + ) -> Result<(), PlayerError> { + // Early out if any request for processing should be ignored. + if self.locked.get() { + return Ok(()); + } + + while let Some(buffer) = self.buffers.pop_front() { + match buffer { + DataBuffer::Payload(payload) => { + if let Err(e) = player.lock().unwrap().push_data(payload) { + warn!("Could not push input data to player {:?}", e); + return Err(e); + } + }, + DataBuffer::EndOfStream => { + if let Err(e) = player.lock().unwrap().end_of_stream() { + warn!("Could not signal EOS to player {:?}", e); + return Err(e); + } + }, + } + } + + Ok(()) + } + + fn reset(&mut self) { + self.locked.set(true); + self.buffers.clear(); + } +} + /// Indicates the reason why a fetch request was cancelled. #[derive(Debug, MallocSizeOf, PartialEq)] enum CancelReason { @@ -2672,12 +2837,16 @@ enum CancelReason { #[derive(MallocSizeOf)] pub(crate) struct HTMLMediaElementFetchContext { + /// The fetch request id. + request_id: RequestId, /// Some if the request has been cancelled. cancel_reason: Option<CancelReason>, /// Indicates whether the fetched stream is seekable. is_seekable: bool, /// Indicates whether the fetched stream is origin clean. origin_clean: bool, + /// The buffered data source which to be processed by media backend. + data_source: DomRefCell<BufferedDataSource>, /// Fetch canceller. Allows cancelling the current fetch request by /// manually calling its .cancel() method or automatically on Drop. fetch_canceller: FetchCanceller, @@ -2686,13 +2855,19 @@ pub(crate) struct HTMLMediaElementFetchContext { impl HTMLMediaElementFetchContext { fn new(request_id: RequestId) -> HTMLMediaElementFetchContext { HTMLMediaElementFetchContext { + request_id, cancel_reason: None, is_seekable: false, origin_clean: true, + data_source: DomRefCell::new(BufferedDataSource::new()), fetch_canceller: FetchCanceller::new(request_id), } } + fn request_id(&self) -> RequestId { + self.request_id + } + fn is_seekable(&self) -> bool { self.is_seekable } @@ -2709,11 +2884,16 @@ impl HTMLMediaElementFetchContext { self.origin_clean = false; } + fn data_source(&self) -> &DomRefCell<BufferedDataSource> { + &self.data_source + } + fn cancel(&mut self, reason: CancelReason) { if self.cancel_reason.is_some() { return; } self.cancel_reason = Some(reason); + self.data_source.borrow_mut().reset(); self.fetch_canceller.cancel(); } @@ -2729,6 +2909,8 @@ struct HTMLMediaElementFetchListener { metadata: Option<Metadata>, /// The generation of the media element when this fetch started. generation_id: u32, + /// The fetch request id. + request_id: RequestId, /// Time of last progress notification. next_progress_event: Instant, /// Timing data for this resource. @@ -2737,16 +2919,12 @@ struct HTMLMediaElementFetchListener { url: ServoUrl, /// Expected content length of the media asset being fetched or played. expected_content_length: Option<u64>, - /// Number of the last byte fetched from the network for the ongoing - /// request. It is only reset to 0 if we reach EOS. Seek requests - /// set it to the requested position. Requests triggered after an - /// EnoughData event uses this value to restart the download from - /// the last fetched position. - latest_fetched_content: u64, - /// The media player discards all data pushes until the seek block - /// is released right before pushing the data from the offset requested - /// by a seek request. - seek_lock: Option<SeekLock>, + /// Actual content length of the media asset was fetched. + fetched_content_length: u64, + /// Discarded content length from the network for the ongoing + /// request if range requests are not supported. Seek requests set it + /// to the required position (in bytes). + content_length_to_discard: u64, } // https://html.spec.whatwg.org/multipage/#media-data-processing-steps-list @@ -2758,11 +2936,6 @@ impl FetchResponseListener for HTMLMediaElementFetchListener { fn process_response(&mut self, _: RequestId, metadata: Result<FetchMetadata, NetworkError>) { let elem = self.elem.root(); - if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() { - // A new fetch request was triggered, so we ignore this response. - return; - } - if let Ok(FetchMetadata::Filtered { filtered: FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_), .. @@ -2794,24 +2967,25 @@ impl FetchResponseListener for HTMLMediaElementFetchListener { // We only set the expected input size if it changes. if content_length != self.expected_content_length { if let Some(content_length) = content_length { - if let Err(e) = elem - .player - .borrow() - .as_ref() - .unwrap() - .lock() - .unwrap() - .set_input_size(content_length) - { - warn!("Could not set player input size {:?}", e); - } else { - self.expected_content_length = Some(content_length); - } + self.expected_content_length = Some(content_length); } } } } + // Explicit media player initialization with live/seekable source. + if let Err(e) = elem + .player + .borrow() + .as_ref() + .unwrap() + .lock() + .unwrap() + .set_input_size(self.expected_content_length.unwrap_or_default()) + { + warn!("Could not set player input size {:?}", e); + } + let (status_is_ok, is_seekable) = self.metadata.as_ref().map_or((true, false), |s| { let status = &s.status; ( @@ -2839,52 +3013,51 @@ impl FetchResponseListener for HTMLMediaElementFetchListener { } } - fn process_response_chunk(&mut self, _: RequestId, payload: Vec<u8>) { + fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) { let elem = self.elem.root(); - // If an error was received previously or if we triggered a new fetch request, - // we skip processing the payload. - if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() { - return; - } - if let Some(ref current_fetch_context) = *elem.current_fetch_context.borrow() { + + self.fetched_content_length += chunk.len() as u64; + + // If an error was received previously, we skip processing the payload. + if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { if current_fetch_context.cancel_reason().is_some() { return; } - } - - let payload_len = payload.len() as u64; - if let Some(seek_lock) = self.seek_lock.take() { - seek_lock.unlock(/* successful seek */ true); - } + // Discard chunk of the response body if fetch context doesn't + // support range requests. + let payload = if !current_fetch_context.is_seekable() && + self.content_length_to_discard != 0 + { + if chunk.len() as u64 > self.content_length_to_discard { + let shrink_chunk = chunk[self.content_length_to_discard as usize..].to_vec(); + self.content_length_to_discard = 0; + shrink_chunk + } else { + // Completely discard this response chunk. + self.content_length_to_discard -= chunk.len() as u64; + return; + } + } else { + chunk + }; - // Push input data into the player. - if let Err(e) = elem - .player - .borrow() - .as_ref() - .unwrap() - .lock() - .unwrap() - .push_data(payload) - { - // If we are pushing too much data and we know that we can - // restart the download later from where we left, we cancel - // the current request. Otherwise, we continue the request - // assuming that we may drop some frames. - if e == PlayerError::EnoughData { - if let Some(ref mut current_fetch_context) = - *elem.current_fetch_context.borrow_mut() - { + if let Err(e) = { + let mut data_source = current_fetch_context.data_source().borrow_mut(); + data_source.add_buffer_to_queue(DataBuffer::Payload(payload)); + data_source.process_into_player_from_queue(elem.player.borrow().as_ref().unwrap()) + } { + // If we are pushing too much data and we know that we can + // restart the download later from where we left, we cancel + // the current request. Otherwise, we continue the request + // assuming that we may drop some frames. + if e == PlayerError::EnoughData { current_fetch_context.cancel(CancelReason::Backoff); } + return; } - warn!("Could not push input data to player {:?}", e); - return; } - self.latest_fetched_content += payload_len; - // https://html.spec.whatwg.org/multipage/#concept-media-load-resource step 4, // => "If mode is remote" step 2 if Instant::now() > self.next_progress_event { @@ -2903,38 +3076,45 @@ impl FetchResponseListener for HTMLMediaElementFetchListener { status: Result<ResourceFetchTiming, NetworkError>, ) { trace!("process response eof"); - if let Some(seek_lock) = self.seek_lock.take() { - seek_lock.unlock(/* successful seek */ false); - } let elem = self.elem.root(); - if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() { - return; - } - // There are no more chunks of the response body forthcoming, so we can // go ahead and notify the media backend not to expect any further data. - if let Err(e) = elem - .player - .borrow() - .as_ref() - .unwrap() - .lock() - .unwrap() - .end_of_stream() - { - warn!("Could not signal EOS to player {:?}", e); - } + if let Some(ref mut current_fetch_context) = *elem.current_fetch_context.borrow_mut() { + // On initial state change READY -> PAUSED the media player perform + // seek to initial position by event with seek segment (TIME format) + // while media stack operates in BYTES format and configuring segment + // start and stop positions without the total size of the stream is not + // possible. As fallback the media player perform seek with BYTES format + // and initiate seek request via "seek-data" callback with required offset. + if self.expected_content_length.is_none() && self.fetched_content_length != 0 { + if let Err(e) = elem + .player + .borrow() + .as_ref() + .unwrap() + .lock() + .unwrap() + .set_input_size(self.fetched_content_length) + { + warn!("Could not set player input size {:?}", e); + } + } + + let mut data_source = current_fetch_context.data_source().borrow_mut(); - // If an error was previously received we skip processing the payload. - if let Some(ref current_fetch_context) = *elem.current_fetch_context.borrow() { + data_source.add_buffer_to_queue(DataBuffer::EndOfStream); + let _ = + data_source.process_into_player_from_queue(elem.player.borrow().as_ref().unwrap()); + + // If an error was previously received we skip processing the payload. if let Some(CancelReason::Error) = current_fetch_context.cancel_reason() { return; } } - if status.is_ok() && self.latest_fetched_content != 0 { + if status.is_ok() && self.fetched_content_length != 0 { elem.upcast::<EventTarget>() .fire_event(atom!("progress"), CanGc::note()); @@ -3015,28 +3195,33 @@ impl ResourceTimingListener for HTMLMediaElementFetchListener { impl PreInvoke for HTMLMediaElementFetchListener { fn should_invoke(&self) -> bool { - //TODO: finish_load needs to run at some point if the generation changes. - self.elem.root().generation_id.get() == self.generation_id + let elem = self.elem.root(); + + if elem.generation_id.get() != self.generation_id || elem.player.borrow().is_none() { + return false; + } + + // A new fetch request was triggered, so we skip processing previous request. + elem.current_fetch_context + .borrow() + .as_ref() + .is_some_and(|context| context.request_id() == self.request_id) } } impl HTMLMediaElementFetchListener { - fn new( - elem: &HTMLMediaElement, - url: ServoUrl, - offset: u64, - seek_lock: Option<SeekLock>, - ) -> Self { + fn new(elem: &HTMLMediaElement, request_id: RequestId, url: ServoUrl, offset: u64) -> Self { Self { elem: Trusted::new(elem), metadata: None, generation_id: elem.generation_id.get(), + request_id, next_progress_event: Instant::now() + Duration::from_millis(350), resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), url, expected_content_length: None, - latest_fetched_content: offset, - seek_lock, + fetched_content_length: 0, + content_length_to_discard: offset, } } } diff --git a/components/script/dom/htmlmetaelement.rs b/components/script/dom/htmlmetaelement.rs index e94a5e1ff33..4ec6db7212c 100644 --- a/components/script/dom/htmlmetaelement.rs +++ b/components/script/dom/htmlmetaelement.rs @@ -2,6 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::str::FromStr; + +use compositing_traits::CompositorMsg; +use compositing_traits::viewport_description::ViewportDescription; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; @@ -61,6 +65,9 @@ impl HTMLMetaElement { if name == "referrer" { self.apply_referrer(); } + if name == "viewport" { + self.parse_and_send_viewport_if_necessary(); + } // https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv } else if !self.HttpEquiv().is_empty() { // TODO: Implement additional http-equiv candidates @@ -115,6 +122,29 @@ impl HTMLMetaElement { } } + /// <https://drafts.csswg.org/css-viewport/#parsing-algorithm> + fn parse_and_send_viewport_if_necessary(&self) { + // Skip processing if this isn't the top level frame + if !self.owner_window().is_top_level() { + return; + } + let element = self.upcast::<Element>(); + let Some(content) = element.get_attribute(&ns!(), &local_name!("content")) else { + return; + }; + + if let Ok(viewport) = ViewportDescription::from_str(&content.value()) { + self.owner_window() + .compositor_api() + .sender() + .send(CompositorMsg::Viewport( + self.owner_window().webview_id(), + viewport, + )) + .unwrap(); + } + } + /// <https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv-content-security-policy> fn apply_csp_list(&self) { if let Some(parent) = self.upcast::<Node>().GetParentElement() { diff --git a/components/script/dom/htmlscriptelement.rs b/components/script/dom/htmlscriptelement.rs index d1b3cfd3467..b3005b181da 100644 --- a/components/script/dom/htmlscriptelement.rs +++ b/components/script/dom/htmlscriptelement.rs @@ -70,6 +70,7 @@ use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::trustedscript::TrustedScript; use crate::dom::trustedscripturl::TrustedScriptURL; use crate::dom::virtualmethods::VirtualMethods; +use crate::dom::window::Window; use crate::fetch::create_a_potential_cors_request; use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingListener}; use crate::realms::enter_realm; @@ -275,6 +276,7 @@ pub(crate) static SCRIPT_JS_MIMES: StaticStringVec = &[ pub(crate) enum ScriptType { Classic, Module, + ImportMap, } #[derive(JSTraceable, MallocSizeOf)] @@ -770,14 +772,15 @@ impl HTMLScriptElement { // Step 23. Module script credentials mode. let module_credentials_mode = match script_type { ScriptType::Classic => CredentialsMode::CredentialsSameOrigin, - ScriptType::Module => reflect_cross_origin_attribute(element).map_or( - CredentialsMode::CredentialsSameOrigin, - |attr| match &*attr { - "use-credentials" => CredentialsMode::Include, - "anonymous" => CredentialsMode::CredentialsSameOrigin, - _ => CredentialsMode::CredentialsSameOrigin, - }, - ), + ScriptType::Module | ScriptType::ImportMap => reflect_cross_origin_attribute(element) + .map_or( + CredentialsMode::CredentialsSameOrigin, + |attr| match &*attr { + "use-credentials" => CredentialsMode::Include, + "anonymous" => CredentialsMode::CredentialsSameOrigin, + _ => CredentialsMode::CredentialsSameOrigin, + }, + ), }; // Step 24. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value. @@ -821,7 +824,13 @@ impl HTMLScriptElement { if let Some(src) = element.get_attribute(&ns!(), &local_name!("src")) { // Step 31. If el has a src content attribute, then: - // TODO: Step 31.1. If el's type is "importmap". + // Step 31.1. If el's type is "importmap". + if script_type == ScriptType::ImportMap { + // then queue an element task on the DOM manipulation task source + // given el to fire an event named error at el, and return. + self.queue_error_event(); + return; + } // Step 31.2. Let src be the value of el's src attribute. let src = src.value(); @@ -904,7 +913,7 @@ impl HTMLScriptElement { doc.add_asap_script(self); }; }, - // TODO: Case "importmap" + ScriptType::ImportMap => (), } } else { // Step 32. If el does not have a src content attribute: @@ -913,18 +922,17 @@ impl HTMLScriptElement { let text_rc = Rc::new(text); - // TODO: Fix step number or match spec text. Is this step 32.1? - let result = Ok(ScriptOrigin::internal( - Rc::clone(&text_rc), - base_url.clone(), - options.clone(), - script_type, - self.global().unminified_js_dir(), - )); - - // TODO: Fix step number or match spec text. Is this step 32.2? + // Step 32.2: Switch on el's type: match script_type { ScriptType::Classic => { + let result = Ok(ScriptOrigin::internal( + Rc::clone(&text_rc), + base_url.clone(), + options.clone(), + script_type, + self.global().unminified_js_dir(), + )); + if was_parser_inserted && doc.get_current_parser() .is_some_and(|parser| parser.script_nesting_level() <= 1) && @@ -958,6 +966,10 @@ impl HTMLScriptElement { can_gc, ); }, + ScriptType::ImportMap => { + // TODO: Let result be the result of creating an import map + // parse result given source text and base URL. + }, } } } @@ -1054,19 +1066,17 @@ impl HTMLScriptElement { } else { document.set_current_script(Some(self)) } - }, - ScriptType::Module => document.set_current_script(None), - } - - match script.type_ { - ScriptType::Classic => { self.run_a_classic_script(&script, can_gc); document.set_current_script(old_script.as_deref()); }, ScriptType::Module => { - assert!(document.GetCurrentScript().is_none()); + document.set_current_script(None); self.run_a_module_script(&script, false, can_gc); }, + ScriptType::ImportMap => { + // TODO: Register an import map given el's relevant + // global object and el's result. + }, } // Step 7. @@ -1517,6 +1527,13 @@ impl HTMLScriptElementMethods<crate::DomTypeHolder> for HTMLScriptElement { .SetTextContent(Some(DOMString::from(value)), can_gc); Ok(()) } + + /// <https://html.spec.whatwg.org/multipage/#dom-script-supports> + fn Supports(_window: &Window, type_: DOMString) -> bool { + // The type argument has to exactly match these values, + // we do not perform an ASCII case-insensitive match. + matches!(type_.str(), "classic" | "module" | "importmap") + } } #[derive(Clone, Copy)] diff --git a/components/script/dom/htmlselectelement.rs b/components/script/dom/htmlselectelement.rs index 0f48c8e923f..2fd8f35c4d8 100644 --- a/components/script/dom/htmlselectelement.rs +++ b/components/script/dom/htmlselectelement.rs @@ -16,6 +16,8 @@ use embedder_traits::{SelectElementOptionOrOptgroup, SelectElementOption}; use euclid::{Size2D, Point2D, Rect}; use embedder_traits::{FormControl as EmbedderFormControl, EmbedderMsg}; +use crate::dom::bindings::refcounted::Trusted; +use crate::dom::event::{EventBubbles, EventCancelable, EventComposed}; use crate::dom::bindings::codegen::GenericBindings::HTMLOptGroupElementBinding::HTMLOptGroupElement_Binding::HTMLOptGroupElementMethods; use crate::dom::activation::Activatable; use crate::dom::attr::Attr; @@ -346,13 +348,6 @@ impl HTMLSelectElement { .SetData(displayed_text.trim().into()); } - pub(crate) fn selection_changed(&self, can_gc: CanGc) { - self.update_shadow_tree(can_gc); - - self.upcast::<EventTarget>() - .fire_bubbling_event(atom!("change"), can_gc); - } - pub(crate) fn selected_option(&self) -> Option<DomRoot<HTMLOptionElement>> { self.list_of_options() .find(|opt_elem| opt_elem.Selected()) @@ -417,12 +412,39 @@ impl HTMLSelectElement { return None; }; - if response.is_some() && response != selected_index { - self.selection_changed(can_gc); - } - response } + + /// <https://html.spec.whatwg.org/multipage/#send-select-update-notifications> + fn send_update_notifications(&self) { + // > When the user agent is to send select update notifications, queue an element task on the + // > user interaction task source given the select element to run these steps: + let this = Trusted::new(self); + self.owner_global() + .task_manager() + .user_interaction_task_source() + .queue(task!(send_select_update_notification: move || { + let this = this.root(); + + // TODO: Step 1. Set the select element's user validity to true. + + // Step 2. Fire an event named input at the select element, with the bubbles and composed + // attributes initialized to true. + this.upcast::<EventTarget>() + .fire_event_with_params( + atom!("input"), + EventBubbles::Bubbles, + EventCancelable::NotCancelable, + EventComposed::Composed, + CanGc::note(), + ); + + // Step 3. Fire an event named change at the select element, with the bubbles attribute initialized + // to true. + this.upcast::<EventTarget>() + .fire_bubbling_event(atom!("change"), CanGc::note()); + })); + } } impl HTMLSelectElementMethods<crate::DomTypeHolder> for HTMLSelectElement { @@ -578,21 +600,28 @@ impl HTMLSelectElementMethods<crate::DomTypeHolder> for HTMLSelectElement { /// <https://html.spec.whatwg.org/multipage/#dom-select-selectedindex> fn SetSelectedIndex(&self, index: i32, can_gc: CanGc) { + let mut selection_did_change = false; + let mut opt_iter = self.list_of_options(); for opt in opt_iter.by_ref().take(index as usize) { + selection_did_change |= opt.Selected(); opt.set_selectedness(false); } - if let Some(opt) = opt_iter.next() { - opt.set_selectedness(true); - opt.set_dirtiness(true); + if let Some(selected_option) = opt_iter.next() { + selection_did_change |= !selected_option.Selected(); + selected_option.set_selectedness(true); + selected_option.set_dirtiness(true); + // Reset remaining <option> elements for opt in opt_iter { + selection_did_change |= opt.Selected(); opt.set_selectedness(false); } } - // TODO: Track whether the selected element actually changed - self.update_shadow_tree(can_gc); + if selection_did_change { + self.update_shadow_tree(can_gc); + } } /// <https://html.spec.whatwg.org/multipage/#dom-cva-willvalidate> @@ -767,7 +796,6 @@ impl Activatable for HTMLSelectElement { true } - /// <https://html.spec.whatwg.org/multipage/#input-activation-behavior> fn activation_behavior(&self, _event: &Event, _target: &EventTarget, can_gc: CanGc) { let Some(selected_value) = self.show_menu(can_gc) else { // The user did not select a value @@ -775,6 +803,7 @@ impl Activatable for HTMLSelectElement { }; self.SetSelectedIndex(selected_value as i32, can_gc); + self.send_update_notifications(); } } diff --git a/components/script/dom/htmlvideoelement.rs b/components/script/dom/htmlvideoelement.rs index c5699014ee2..622e5079ee9 100644 --- a/components/script/dom/htmlvideoelement.rs +++ b/components/script/dom/htmlvideoelement.rs @@ -37,7 +37,7 @@ use crate::dom::bindings::str::DOMString; use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; use crate::dom::globalscope::GlobalScope; -use crate::dom::htmlmediaelement::{HTMLMediaElement, ReadyState}; +use crate::dom::htmlmediaelement::{HTMLMediaElement, NetworkState, ReadyState}; use crate::dom::node::{Node, NodeTraits}; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::virtualmethods::VirtualMethods; @@ -294,6 +294,10 @@ impl HTMLVideoElement { pub(crate) fn origin_is_clean(&self) -> bool { self.htmlmediaelement.origin_is_clean() } + + pub(crate) fn is_network_state_empty(&self) -> bool { + self.htmlmediaelement.network_state() == NetworkState::Empty + } } impl HTMLVideoElementMethods<crate::DomTypeHolder> for HTMLVideoElement { diff --git a/components/script/dom/imagebitmap.rs b/components/script/dom/imagebitmap.rs index cddd8cf1188..0a43bf4f176 100644 --- a/components/script/dom/imagebitmap.rs +++ b/components/script/dom/imagebitmap.rs @@ -68,7 +68,7 @@ impl ImageBitmap { /// Return the value of the [`[[Detached]]`](https://html.spec.whatwg.org/multipage/#detached) /// internal slot - fn is_detached(&self) -> bool { + pub(crate) fn is_detached(&self) -> bool { self.bitmap_data.borrow().is_none() } } @@ -109,9 +109,9 @@ impl Serializable for ImageBitmap { } fn serialized_storage<'a>( - reader: StructuredData<'a, '_>, + data: StructuredData<'a, '_>, ) -> &'a mut Option<HashMap<ImageBitmapId, Self::Data>> { - match reader { + match data { StructuredData::Reader(r) => &mut r.image_bitmaps, StructuredData::Writer(w) => &mut w.image_bitmaps, } diff --git a/components/script/dom/imagedata.rs b/components/script/dom/imagedata.rs index a891064952a..907561cdda4 100644 --- a/components/script/dom/imagedata.rs +++ b/components/script/dom/imagedata.rs @@ -41,14 +41,11 @@ impl ImageData { mut data: Option<Vec<u8>>, can_gc: CanGc, ) -> Fallible<DomRoot<ImageData>> { - // The color components of each pixel must be stored in four sequential - // elements in the order of red, green, blue, and then alpha. - let len = 4u32 - .checked_mul(width) - .and_then(|v| v.checked_mul(height)) - .ok_or(Error::Range( - "The requested image size exceeds the supported range".to_owned(), - ))?; + let len = + pixels::compute_rgba8_byte_length_if_within_limit(width as usize, height as usize) + .ok_or(Error::Range( + "The requested image size exceeds the supported range".to_owned(), + ))?; unsafe { let cx = GlobalScope::get_cx(); @@ -129,18 +126,16 @@ impl ImageData { return Err(Error::IndexSize); } - // The color components of each pixel must be stored in four sequential - // elements in the order of red, green, blue, and then alpha. - // Please note when a too-large ImageData is created using a constructor - // historically throwns an IndexSizeError, instead of RangeError. - let len = 4u32 - .checked_mul(width) - .and_then(|v| v.checked_mul(height)) - .ok_or(Error::IndexSize)?; + // When a constructor is called for an ImageData that is too large, other browsers throw + // IndexSizeError rather than RangeError here, so we do the same. + let len = + pixels::compute_rgba8_byte_length_if_within_limit(width as usize, height as usize) + .ok_or(Error::IndexSize)?; let cx = GlobalScope::get_cx(); - let heap_typed_array = create_heap_buffer_source_with_length::<ClampedU8>(cx, len, can_gc)?; + let heap_typed_array = + create_heap_buffer_source_with_length::<ClampedU8>(cx, len as u32, can_gc)?; let imagedata = Box::new(ImageData { reflector_: Reflector::new(), @@ -153,14 +148,19 @@ impl ImageData { imagedata, global, proto, can_gc, )) } + + pub(crate) fn is_detached(&self) -> bool { + self.data.is_detached_buffer(GlobalScope::get_cx()) + } + #[allow(unsafe_code)] pub(crate) fn to_shared_memory(&self) -> IpcSharedMemory { IpcSharedMemory::from_bytes(unsafe { self.as_slice() }) } #[allow(unsafe_code)] - pub(crate) unsafe fn get_rect(&self, rect: Rect<u64>) -> Cow<[u8]> { - pixels::rgba8_get_rect(self.as_slice(), self.get_size().to_u64(), rect) + pub(crate) unsafe fn get_rect(&self, rect: Rect<u32>) -> Cow<[u8]> { + pixels::rgba8_get_rect(self.as_slice(), self.get_size().to_u32(), rect) } pub(crate) fn get_size(&self) -> Size2D<u32> { diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 691b3a38f19..6d4a0d2529e 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -48,7 +48,7 @@ use style::properties::ComputedValues; use style::selector_parser::{SelectorImpl, SelectorParser}; use style::stylesheets::{Stylesheet, UrlExtraData}; use uuid::Uuid; -use xml5ever::serialize as xml_serialize; +use xml5ever::{local_name, serialize as xml_serialize}; use crate::conversions::Convert; use crate::document_loader::DocumentLoader; @@ -738,10 +738,9 @@ impl Node { } pub(crate) fn ranges_is_empty(&self) -> bool { - match self.rare_data().as_ref() { - Some(data) => data.ranges.is_empty(), - None => false, - } + self.rare_data() + .as_ref() + .is_none_or(|data| data.ranges.is_empty()) } #[inline] @@ -1247,13 +1246,13 @@ impl Node { } } - pub(crate) fn unique_id(&self) -> String { + pub(crate) fn unique_id(&self, pipeline: PipelineId) -> String { let mut rare_data = self.ensure_rare_data(); if rare_data.unique_id.is_none() { - let id = UniqueId::new(); - ScriptThread::save_node_id(id.borrow().simple().to_string()); - rare_data.unique_id = Some(id); + let node_id = UniqueId::new(); + ScriptThread::save_node_id(pipeline, node_id.borrow().simple().to_string()); + rare_data.unique_id = Some(node_id); } rare_data .unique_id @@ -1267,6 +1266,7 @@ impl Node { pub(crate) fn summarize(&self, can_gc: CanGc) -> NodeInfo { let USVString(base_uri) = self.BaseURI(); let node_type = self.NodeType(); + let pipeline = self.owner_document().window().pipeline_id(); let maybe_shadow_root = self.downcast::<ShadowRoot>(); let shadow_root_mode = maybe_shadow_root @@ -1274,7 +1274,7 @@ impl Node { .map(ShadowRootMode::convert); let host = maybe_shadow_root .map(ShadowRoot::Host) - .map(|host| host.upcast::<Node>().unique_id()); + .map(|host| host.upcast::<Node>().unique_id(pipeline)); let is_shadow_host = self.downcast::<Element>().is_some_and(|potential_host| { let Some(root) = potential_host.shadow_root() else { return false; @@ -1296,12 +1296,12 @@ impl Node { .map(|style| style.Display().into()); NodeInfo { - unique_id: self.unique_id(), + unique_id: self.unique_id(pipeline), host, base_uri, parent: self .GetParentNode() - .map_or("".to_owned(), |node| node.unique_id()), + .map_or("".to_owned(), |node| node.unique_id(pipeline)), node_type, is_top_level_document: node_type == NodeConstants::DOCUMENT_NODE, node_name: String::from(self.NodeName()), @@ -1455,6 +1455,21 @@ impl Node { .map(|data| data.element_data.borrow().styles.primary().clone()) } + /// <https://html.spec.whatwg.org/multipage/#language> + pub(crate) fn get_lang(&self) -> Option<String> { + self.inclusive_ancestors(ShadowIncluding::Yes) + .filter_map(|node| { + node.downcast::<Element>().and_then(|el| { + el.get_attribute(&ns!(xml), &local_name!("lang")) + .or_else(|| el.get_attribute(&ns!(), &local_name!("lang"))) + .map(|attr| String::from(attr.Value())) + }) + // TODO: Check meta tags for a pragma-set default language + // TODO: Check HTTP Content-Language header + }) + .next() + } + /// <https://dom.spec.whatwg.org/#assign-slotables-for-a-tree> pub(crate) fn assign_slottables_for_a_tree(&self) { // NOTE: This method traverses all descendants of the node and is potentially very @@ -1568,6 +1583,7 @@ pub(crate) unsafe fn from_untrusted_node_address(candidate: UntrustedNodeAddress pub(crate) trait LayoutNodeHelpers<'dom> { fn type_id_for_layout(self) -> NodeTypeId; + fn parent_node_ref(self) -> Option<LayoutDom<'dom, Node>>; fn composed_parent_node_ref(self) -> Option<LayoutDom<'dom, Node>>; fn first_child_ref(self) -> Option<LayoutDom<'dom, Node>>; fn last_child_ref(self) -> Option<LayoutDom<'dom, Node>>; @@ -1630,7 +1646,7 @@ pub(crate) trait LayoutNodeHelpers<'dom> { impl<'dom> LayoutDom<'dom, Node> { #[inline] #[allow(unsafe_code)] - fn parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> { + pub(crate) fn parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> { unsafe { self.unsafe_get().parent_node.get_inner_as_layout() } } } @@ -1647,6 +1663,12 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> { } #[inline] + #[allow(unsafe_code)] + fn parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> { + unsafe { self.unsafe_get().parent_node.get_inner_as_layout() } + } + + #[inline] fn composed_parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> { let parent = self.parent_node_ref(); if let Some(parent) = parent { @@ -2603,7 +2625,9 @@ impl Node { fn remove(node: &Node, parent: &Node, suppress_observers: SuppressObserver, can_gc: CanGc) { parent.owner_doc().add_script_and_layout_blocker(); - // Step 2. + // Step 1. Let parent be node’s parent. + // Step 2. Assert: parent is non-null. + // NOTE: We get parent as an argument instead assert!( node.GetParentNode() .is_some_and(|node_parent| &*node_parent == parent) @@ -2615,11 +2639,21 @@ impl Node { if parent.ranges_is_empty() { None } else { - // Step 1. + // Step 1. Let parent be node’s parent. + // Step 2. Assert: parent is not null. + // NOTE: We already have the parent. + + // Step 3. Let index be node’s index. let index = node.index(); - // Steps 2-3 are handled in Node::unbind_from_tree. - // Steps 4-5. + + // Steps 4-5 are handled in Node::unbind_from_tree. + + // Step 6. For each live range whose start node is parent and start offset is greater than index, + // decrease its start offset by 1. + // Step 7. For each live range whose end node is parent and end offset is greater than index, + // decrease its end offset by 1. parent.ranges().decrease_above(parent, index, 1); + // Parent had ranges, we needed the index, let's keep track of // it to avoid computing it for other ranges when calling // unbind_from_tree recursively. @@ -3863,7 +3897,12 @@ impl VirtualMethods for Node { /// <https://dom.spec.whatwg.org/#concept-node-remove> fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { self.super_type().unwrap().unbind_from_tree(context, can_gc); - if !self.ranges_is_empty() { + + // Ranges should only drain to the parent from inclusive non-shadow + // including descendants. If we're in a shadow tree at this point then the + // unbind operation happened further up in the tree and we should not + // drain any ranges. + if !self.is_in_a_shadow_tree() && !self.ranges_is_empty() { self.ranges().drain_to_parent(context, self); } } diff --git a/components/script/dom/offscreencanvas.rs b/components/script/dom/offscreencanvas.rs index b4323ef6b54..391c83f3a84 100644 --- a/components/script/dom/offscreencanvas.rs +++ b/components/script/dom/offscreencanvas.rs @@ -72,8 +72,11 @@ impl OffscreenCanvas { ) } - pub(crate) fn get_size(&self) -> Size2D<u64> { - Size2D::new(self.Width(), self.Height()) + pub(crate) fn get_size(&self) -> Size2D<u32> { + Size2D::new( + self.Width().try_into().unwrap_or(u32::MAX), + self.Height().try_into().unwrap_or(u32::MAX), + ) } pub(crate) fn origin_is_clean(&self) -> bool { @@ -92,7 +95,13 @@ impl OffscreenCanvas { Some(context) => context.get_image_data(), None => { let size = self.get_size(); - if size.width == 0 || size.height == 0 { + if size.is_empty() || + pixels::compute_rgba8_byte_length_if_within_limit( + size.width as usize, + size.height as usize, + ) + .is_none() + { None } else { Some(Snapshot::cleared(size)) @@ -117,10 +126,6 @@ impl OffscreenCanvas { Some(context) } - pub(crate) fn is_valid(&self) -> bool { - self.Width() != 0 && self.Height() != 0 - } - pub(crate) fn placeholder(&self) -> Option<&HTMLCanvasElement> { self.placeholder.as_deref() } diff --git a/components/script/dom/range.rs b/components/script/dom/range.rs index 4bf3b7fb72a..481c2f286c6 100644 --- a/components/script/dom/range.rs +++ b/components/script/dom/range.rs @@ -2,14 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::UnsafeCell; +use std::cell::RefCell; use std::cmp::{Ordering, PartialOrd}; use std::iter; use dom_struct::dom_struct; use js::jsapi::JSTracer; use js::rust::HandleObject; -use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; use crate::dom::abstractrange::{AbstractRange, BoundaryPoint, bp_position}; use crate::dom::bindings::cell::DomRefCell; @@ -344,6 +343,58 @@ impl Range { .chain(iter::once(end_clone)) .flat_map(move |node| node.content_boxes(can_gc)) } + + /// <https://dom.spec.whatwg.org/#concept-range-bp-set> + #[allow(clippy::neg_cmp_op_on_partial_ord)] + fn set_the_start_or_end( + &self, + node: &Node, + offset: u32, + start_or_end: StartOrEnd, + ) -> ErrorResult { + // Step 1. If node is a doctype, then throw an "InvalidNodeTypeError" DOMException. + if node.is_doctype() { + return Err(Error::InvalidNodeType); + } + + // Step 2. If offset is greater than node’s length, then throw an "IndexSizeError" DOMException. + if offset > node.len() { + return Err(Error::IndexSize); + } + + // Step 3. Let bp be the boundary point (node, offset). + // NOTE: We don't need this part. + + match start_or_end { + // If these steps were invoked as "set the start" + StartOrEnd::Start => { + // Step 4.1 If range’s root is not equal to node’s root, or if bp is after the range’s end, + // set range’s end to bp. + // Step 4.2 Set range’s start to bp. + self.set_start(node, offset); + if !(self.start() <= self.end()) { + self.set_end(node, offset); + } + }, + // If these steps were invoked as "set the end" + StartOrEnd::End => { + // Step 4.1 If range’s root is not equal to node’s root, or if bp is before the range’s start, + // set range’s start to bp. + // Step 4.2 Set range’s end to bp. + self.set_end(node, offset); + if !(self.end() >= self.start()) { + self.set_start(node, offset); + } + }, + } + + Ok(()) + } +} + +enum StartOrEnd { + Start, + End, } impl RangeMethods<crate::DomTypeHolder> for Range { @@ -365,43 +416,13 @@ impl RangeMethods<crate::DomTypeHolder> for Range { } /// <https://dom.spec.whatwg.org/#dom-range-setstart> - #[allow(clippy::neg_cmp_op_on_partial_ord)] fn SetStart(&self, node: &Node, offset: u32) -> ErrorResult { - if node.is_doctype() { - // Step 1. - Err(Error::InvalidNodeType) - } else if offset > node.len() { - // Step 2. - Err(Error::IndexSize) - } else { - // Step 3. - self.set_start(node, offset); - if !(self.start() <= self.end()) { - // Step 4. - self.set_end(node, offset); - } - Ok(()) - } + self.set_the_start_or_end(node, offset, StartOrEnd::Start) } /// <https://dom.spec.whatwg.org/#dom-range-setend> - #[allow(clippy::neg_cmp_op_on_partial_ord)] fn SetEnd(&self, node: &Node, offset: u32) -> ErrorResult { - if node.is_doctype() { - // Step 1. - Err(Error::InvalidNodeType) - } else if offset > node.len() { - // Step 2. - Err(Error::IndexSize) - } else { - // Step 3. - self.set_end(node, offset); - if !(self.end() >= self.start()) { - // Step 4. - self.set_start(node, offset); - } - Ok(()) - } + self.set_the_start_or_end(node, offset, StartOrEnd::End) } /// <https://dom.spec.whatwg.org/#dom-range-setstartbefore> @@ -1172,14 +1193,15 @@ impl RangeMethods<crate::DomTypeHolder> for Range { } } +#[derive(MallocSizeOf)] pub(crate) struct WeakRangeVec { - cell: UnsafeCell<WeakRefVec<Range>>, + cell: RefCell<WeakRefVec<Range>>, } impl Default for WeakRangeVec { fn default() -> Self { WeakRangeVec { - cell: UnsafeCell::new(WeakRefVec::new()), + cell: RefCell::new(WeakRefVec::new()), } } } @@ -1188,7 +1210,7 @@ impl Default for WeakRangeVec { impl WeakRangeVec { /// Whether that vector of ranges is empty. pub(crate) fn is_empty(&self) -> bool { - unsafe { (*self.cell.get()).is_empty() } + self.cell.borrow().is_empty() } /// Used for steps 2.1-2. when inserting a node. @@ -1204,6 +1226,7 @@ impl WeakRangeVec { } /// Used for steps 2-3. when removing a node. + /// /// <https://dom.spec.whatwg.org/#concept-node-remove> pub(crate) fn drain_to_parent(&self, context: &UnbindContext, child: &Node) { if self.is_empty() { @@ -1212,26 +1235,29 @@ impl WeakRangeVec { let offset = context.index(); let parent = context.parent; - unsafe { - let ranges = &mut *self.cell.get(); + let ranges = &mut *self.cell.borrow_mut(); - ranges.update(|entry| { - let range = entry.root().unwrap(); - if range.start().node() == parent || range.end().node() == parent { - entry.remove(); - } - if range.start().node() == child { - range.report_change(); - range.start().set(context.parent, offset); - } - if range.end().node() == child { - range.report_change(); - range.end().set(context.parent, offset); - } - }); + ranges.update(|entry| { + let range = entry.root().unwrap(); + if range.start().node() == parent || range.end().node() == parent { + entry.remove(); + } + if range.start().node() == child { + range.report_change(); + range.start().set(context.parent, offset); + } + if range.end().node() == child { + range.report_change(); + range.end().set(context.parent, offset); + } + }); - (*context.parent.ranges().cell.get()).extend(ranges.drain(..)); - } + context + .parent + .ranges() + .cell + .borrow_mut() + .extend(ranges.drain(..)); } /// Used for steps 7.1-2. when normalizing a node. @@ -1241,26 +1267,24 @@ impl WeakRangeVec { return; } - unsafe { - let ranges = &mut *self.cell.get(); + let ranges = &mut *self.cell.borrow_mut(); - ranges.update(|entry| { - let range = entry.root().unwrap(); - if range.start().node() == sibling || range.end().node() == sibling { - entry.remove(); - } - if range.start().node() == node { - range.report_change(); - range.start().set(sibling, range.start_offset() + length); - } - if range.end().node() == node { - range.report_change(); - range.end().set(sibling, range.end_offset() + length); - } - }); + ranges.update(|entry| { + let range = entry.root().unwrap(); + if range.start().node() == sibling || range.end().node() == sibling { + entry.remove(); + } + if range.start().node() == node { + range.report_change(); + range.start().set(sibling, range.start_offset() + length); + } + if range.end().node() == node { + range.report_change(); + range.end().set(sibling, range.end_offset() + length); + } + }); - (*sibling.ranges().cell.get()).extend(ranges.drain(..)); - } + sibling.ranges().cell.borrow_mut().extend(ranges.drain(..)); } /// Used for steps 7.3-4. when normalizing a node. @@ -1272,43 +1296,42 @@ impl WeakRangeVec { child: &Node, new_offset: u32, ) { - unsafe { - let child_ranges = &mut *child.ranges().cell.get(); + let child_ranges = child.ranges(); + let mut child_ranges = child_ranges.cell.borrow_mut(); - (*self.cell.get()).update(|entry| { - let range = entry.root().unwrap(); + self.cell.borrow_mut().update(|entry| { + let range = entry.root().unwrap(); - let node_is_start = range.start().node() == node; - let node_is_end = range.end().node() == node; + let node_is_start = range.start().node() == node; + let node_is_end = range.end().node() == node; - let move_start = node_is_start && range.start_offset() == offset; - let move_end = node_is_end && range.end_offset() == offset; + let move_start = node_is_start && range.start_offset() == offset; + let move_end = node_is_end && range.end_offset() == offset; - let remove_from_node = - move_start && (move_end || !node_is_end) || move_end && !node_is_start; + let remove_from_node = + move_start && (move_end || !node_is_end) || move_end && !node_is_start; - let already_in_child = range.start().node() == child || range.end().node() == child; - let push_to_child = !already_in_child && (move_start || move_end); + let already_in_child = range.start().node() == child || range.end().node() == child; + let push_to_child = !already_in_child && (move_start || move_end); - if remove_from_node { - let ref_ = entry.remove(); - if push_to_child { - child_ranges.push(ref_); - } - } else if push_to_child { - child_ranges.push(WeakRef::new(&range)); + if remove_from_node { + let ref_ = entry.remove(); + if push_to_child { + child_ranges.push(ref_); } + } else if push_to_child { + child_ranges.push(WeakRef::new(&range)); + } - if move_start { - range.report_change(); - range.start().set(child, new_offset); - } - if move_end { - range.report_change(); - range.end().set(child, new_offset); - } - }); - } + if move_start { + range.report_change(); + range.start().set(child, new_offset); + } + if move_end { + range.report_change(); + range.end().set(child, new_offset); + } + }); } /// Used for steps 8-11. when replacing character data. @@ -1337,109 +1360,93 @@ impl WeakRangeVec { offset: u32, sibling: &Node, ) { - unsafe { - let sibling_ranges = &mut *sibling.ranges().cell.get(); + let sibling_ranges = sibling.ranges(); + let mut sibling_ranges = sibling_ranges.cell.borrow_mut(); - (*self.cell.get()).update(|entry| { - let range = entry.root().unwrap(); - let start_offset = range.start_offset(); - let end_offset = range.end_offset(); + self.cell.borrow_mut().update(|entry| { + let range = entry.root().unwrap(); + let start_offset = range.start_offset(); + let end_offset = range.end_offset(); - let node_is_start = range.start().node() == node; - let node_is_end = range.end().node() == node; + let node_is_start = range.start().node() == node; + let node_is_end = range.end().node() == node; - let move_start = node_is_start && start_offset > offset; - let move_end = node_is_end && end_offset > offset; + let move_start = node_is_start && start_offset > offset; + let move_end = node_is_end && end_offset > offset; - let remove_from_node = - move_start && (move_end || !node_is_end) || move_end && !node_is_start; + let remove_from_node = + move_start && (move_end || !node_is_end) || move_end && !node_is_start; - let already_in_sibling = - range.start().node() == sibling || range.end().node() == sibling; - let push_to_sibling = !already_in_sibling && (move_start || move_end); + let already_in_sibling = + range.start().node() == sibling || range.end().node() == sibling; + let push_to_sibling = !already_in_sibling && (move_start || move_end); - if remove_from_node { - let ref_ = entry.remove(); - if push_to_sibling { - sibling_ranges.push(ref_); - } - } else if push_to_sibling { - sibling_ranges.push(WeakRef::new(&range)); + if remove_from_node { + let ref_ = entry.remove(); + if push_to_sibling { + sibling_ranges.push(ref_); } + } else if push_to_sibling { + sibling_ranges.push(WeakRef::new(&range)); + } - if move_start { - range.report_change(); - range.start().set(sibling, start_offset - offset); - } - if move_end { - range.report_change(); - range.end().set(sibling, end_offset - offset); - } - }); - } + if move_start { + range.report_change(); + range.start().set(sibling, start_offset - offset); + } + if move_end { + range.report_change(); + range.end().set(sibling, end_offset - offset); + } + }); } /// Used for steps 7.4-5. when splitting a text node. /// <https://dom.spec.whatwg.org/#concept-text-split> pub(crate) fn increment_at(&self, node: &Node, offset: u32) { - unsafe { - (*self.cell.get()).update(|entry| { - let range = entry.root().unwrap(); - if range.start().node() == node && offset == range.start_offset() { - range.report_change(); - range.start().set_offset(offset + 1); - } - if range.end().node() == node && offset == range.end_offset() { - range.report_change(); - range.end().set_offset(offset + 1); - } - }); - } + self.cell.borrow_mut().update(|entry| { + let range = entry.root().unwrap(); + if range.start().node() == node && offset == range.start_offset() { + range.report_change(); + range.start().set_offset(offset + 1); + } + if range.end().node() == node && offset == range.end_offset() { + range.report_change(); + range.end().set_offset(offset + 1); + } + }); } fn map_offset_above<F: FnMut(u32) -> u32>(&self, node: &Node, offset: u32, mut f: F) { - unsafe { - (*self.cell.get()).update(|entry| { - let range = entry.root().unwrap(); - let start_offset = range.start_offset(); - if range.start().node() == node && start_offset > offset { - range.report_change(); - range.start().set_offset(f(start_offset)); - } - let end_offset = range.end_offset(); - if range.end().node() == node && end_offset > offset { - range.report_change(); - range.end().set_offset(f(end_offset)); - } - }); - } + self.cell.borrow_mut().update(|entry| { + let range = entry.root().unwrap(); + let start_offset = range.start_offset(); + if range.start().node() == node && start_offset > offset { + range.report_change(); + range.start().set_offset(f(start_offset)); + } + let end_offset = range.end_offset(); + if range.end().node() == node && end_offset > offset { + range.report_change(); + range.end().set_offset(f(end_offset)); + } + }); } pub(crate) fn push(&self, ref_: WeakRef<Range>) { - unsafe { - (*self.cell.get()).push(ref_); - } + self.cell.borrow_mut().push(ref_); } fn remove(&self, range: &Range) -> WeakRef<Range> { - unsafe { - let ranges = &mut *self.cell.get(); - let position = ranges.iter().position(|ref_| ref_ == range).unwrap(); - ranges.swap_remove(position) - } - } -} - -#[allow(unsafe_code)] -impl MallocSizeOf for WeakRangeVec { - fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { - unsafe { (*self.cell.get()).size_of(ops) } + let mut ranges = self.cell.borrow_mut(); + let position = ranges.iter().position(|ref_| ref_ == range).unwrap(); + ranges.swap_remove(position) } } #[allow(unsafe_code)] unsafe impl JSTraceable for WeakRangeVec { unsafe fn trace(&self, _: *mut JSTracer) { - (*self.cell.get()).retain_alive() + self.cell.borrow_mut().retain_alive() } } diff --git a/components/script/dom/raredata.rs b/components/script/dom/raredata.rs index 0c048956217..2c303d6874f 100644 --- a/components/script/dom/raredata.rs +++ b/components/script/dom/raredata.rs @@ -11,6 +11,7 @@ use crate::dom::bindings::root::{Dom, MutNullableDom}; use crate::dom::customelementregistry::{ CustomElementDefinition, CustomElementReaction, CustomElementState, }; +use crate::dom::domtokenlist::DOMTokenList; use crate::dom::elementinternals::ElementInternals; use crate::dom::htmlslotelement::SlottableData; use crate::dom::intersectionobserver::IntersectionObserverRegistration; @@ -76,4 +77,10 @@ pub(crate) struct ElementRareData { /// > which is initialized to an empty list. This list holds IntersectionObserverRegistration records, which have: pub(crate) registered_intersection_observers: Vec<IntersectionObserverRegistration>, pub(crate) cryptographic_nonce: String, + + /// <https://drafts.csswg.org/css-shadow-parts/#element-forwarded-part-name-list> + pub(crate) forwarded_part_names: Vec<(String, String)>, + + /// <https://drafts.csswg.org/css-shadow-parts/#dom-element-part> + pub(crate) part: MutNullableDom<DOMTokenList>, } diff --git a/components/script/dom/readablestream.rs b/components/script/dom/readablestream.rs index d2c1d853f86..5203a5f0a83 100644 --- a/components/script/dom/readablestream.rs +++ b/components/script/dom/readablestream.rs @@ -1926,7 +1926,7 @@ impl ReadableStreamMethods<crate::DomTypeHolder> for ReadableStream { // If ! IsReadableStreamLocked(this) is true, // return a promise rejected with a TypeError exception. let promise = Promise::new(&global, can_gc); - promise.reject_error(Error::Type("stream is not locked".to_owned()), can_gc); + promise.reject_error(Error::Type("stream is locked".to_owned()), can_gc); promise } else { // Return ! ReadableStreamCancel(this, reason). diff --git a/components/script/dom/webgl2renderingcontext.rs b/components/script/dom/webgl2renderingcontext.rs index d2bbf3bba57..8f2bf9c2fcc 100644 --- a/components/script/dom/webgl2renderingcontext.rs +++ b/components/script/dom/webgl2renderingcontext.rs @@ -534,7 +534,7 @@ impl WebGL2RenderingContext { let src_origin = Point2D::new(x, y); let src_size = Size2D::new(width as u32, height as u32); let fb_size = Size2D::new(fb_width as u32, fb_height as u32); - match pixels::clip(src_origin, src_size.to_u64(), fb_size.to_u64()) { + match pixels::clip(src_origin, src_size.to_u32(), fb_size.to_u32()) { Some(rect) => rect.to_u32(), None => return, } @@ -2183,7 +2183,7 @@ impl WebGL2RenderingContextMethods<crate::DomTypeHolder> for WebGL2RenderingCont let src_origin = Point2D::new(x, y); let src_size = Size2D::new(width as u32, height as u32); let fb_size = Size2D::new(fb_width as u32, fb_height as u32); - if pixels::clip(src_origin, src_size.to_u64(), fb_size.to_u64()).is_none() { + if pixels::clip(src_origin, src_size.to_u32(), fb_size.to_u32()).is_none() { return; } } diff --git a/components/script/dom/webglrenderingcontext.rs b/components/script/dom/webglrenderingcontext.rs index 70463ae1528..aee785b0e12 100644 --- a/components/script/dom/webglrenderingcontext.rs +++ b/components/script/dom/webglrenderingcontext.rs @@ -575,6 +575,23 @@ impl WebGLRenderingContext { pub(crate) fn get_image_pixels(&self, source: TexImageSource) -> Fallible<Option<TexPixels>> { Ok(Some(match source { + TexImageSource::ImageBitmap(bitmap) => { + if !bitmap.origin_is_clean() { + return Err(Error::Security); + } + let Some(snapshot) = bitmap.bitmap_data().clone() else { + return Ok(None); + }; + + let snapshot = snapshot.as_ipc(); + let size = snapshot.size().cast(); + let format = match snapshot.format() { + snapshot::PixelFormat::RGBA => PixelFormat::RGBA8, + snapshot::PixelFormat::BGRA => PixelFormat::BGRA8, + }; + let premultiply = snapshot.alpha_mode().is_premultiplied(); + TexPixels::new(snapshot.to_ipc_shared_memory(), size, format, premultiply) + }, TexImageSource::ImageData(image_data) => TexPixels::new( image_data.to_shared_memory(), image_data.get_size(), @@ -3838,7 +3855,7 @@ impl WebGLRenderingContextMethods<crate::DomTypeHolder> for WebGLRenderingContex let src_origin = Point2D::new(x, y); let src_size = Size2D::new(width as u32, height as u32); let fb_size = Size2D::new(fb_width as u32, fb_height as u32); - let src_rect = match pixels::clip(src_origin, src_size.to_u64(), fb_size.to_u64()) { + let src_rect = match pixels::clip(src_origin, src_size.to_u32(), fb_size.to_u32()) { Some(rect) => rect, None => return, }; diff --git a/components/script/dom/webgpu/gpucanvascontext.rs b/components/script/dom/webgpu/gpucanvascontext.rs index 359b1b14003..9bf43c77bf5 100644 --- a/components/script/dom/webgpu/gpucanvascontext.rs +++ b/components/script/dom/webgpu/gpucanvascontext.rs @@ -167,8 +167,8 @@ impl GPUCanvasContext { // causes FAIL on webgpu:web_platform,canvas,configure:usage:* usage: configuration.usage | GPUTextureUsageConstants::COPY_SRC, size: GPUExtent3D::GPUExtent3DDict(GPUExtent3DDict { - width: size.width as u32, - height: size.height as u32, + width: size.width, + height: size.height, depthOrArrayLayers: 1, }), viewFormats: configuration.viewFormats.clone(), diff --git a/components/script/dom/webxr/xrwebgllayer.rs b/components/script/dom/webxr/xrwebgllayer.rs index 0e93e583c6f..0d32b10cf47 100644 --- a/components/script/dom/webxr/xrwebgllayer.rs +++ b/components/script/dom/webxr/xrwebgllayer.rs @@ -129,10 +129,7 @@ impl XRWebGLLayer { HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(canvas) => canvas.get_size(), HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(canvas) => { let size = canvas.get_size(); - Size2D::new( - size.width.try_into().unwrap_or(0), - size.height.try_into().unwrap_or(0), - ) + Size2D::new(size.width, size.height) }, }; Size2D::from_untyped(size) diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index dcaa025c778..fca40987898 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -1214,7 +1214,7 @@ impl WindowMethods<crate::DomTypeHolder> for Window { self.as_global_scope().queue_function_as_microtask(callback); } - // https://html.spec.whatwg.org/multipage/#dom-createimagebitmap + /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> fn CreateImageBitmap( &self, image: ImageBitmapSource, @@ -1223,7 +1223,30 @@ impl WindowMethods<crate::DomTypeHolder> for Window { ) -> Rc<Promise> { let p = self .as_global_scope() - .create_image_bitmap(image, options, can_gc); + .create_image_bitmap(image, 0, 0, None, None, options, can_gc); + p + } + + /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> + fn CreateImageBitmap_( + &self, + image: ImageBitmapSource, + sx: i32, + sy: i32, + sw: i32, + sh: i32, + options: &ImageBitmapOptions, + can_gc: CanGc, + ) -> Rc<Promise> { + let p = self.as_global_scope().create_image_bitmap( + image, + sx, + sy, + Some(sw), + Some(sh), + options, + can_gc, + ); p } diff --git a/components/script/dom/windowproxy.rs b/components/script/dom/windowproxy.rs index a8decee24ed..648146ac2e9 100644 --- a/components/script/dom/windowproxy.rs +++ b/components/script/dom/windowproxy.rs @@ -466,26 +466,40 @@ impl WindowProxy { features: DOMString, can_gc: CanGc, ) -> Fallible<Option<DomRoot<WindowProxy>>> { - // Step 4. + // Step 5. If target is the empty string, then set target to "_blank". let non_empty_target = match target.as_ref() { "" => DOMString::from("_blank"), _ => target, }; - // Step 5 + // Step 6. Let tokenizedFeatures be the result of tokenizing features. let tokenized_features = tokenize_open_features(features); - // Step 7-9 + // Step 7 - 8. + // If tokenizedFeatures["noreferrer"] exists, then set noreferrer to + // the result of parsing tokenizedFeatures["noreferrer"] as a boolean feature. let noreferrer = parse_open_feature_boolean(&tokenized_features, "noreferrer"); + + // Step 9. Let noopener be the result of getting noopener for window + // open with sourceDocument, tokenizedFeatures, and urlRecord. let noopener = if noreferrer { true } else { parse_open_feature_boolean(&tokenized_features, "noopener") }; - // Step 10, 11 + // (TODO) Step 10. Remove tokenizedFeatures["noopener"] and tokenizedFeatures["noreferrer"]. + + // (TODO) Step 11. Let referrerPolicy be the empty string. + // (TODO) Step 12. If noreferrer is true, then set referrerPolicy to "no-referrer". + + // Step 13 - 14 + // Let targetNavigable and windowType be the result of applying the rules for + // choosing a navigable given target, sourceDocument's node navigable, and noopener. + // If targetNavigable is null, then return null. let (chosen, new) = match self.choose_browsing_context(non_empty_target, noopener) { (Some(chosen), new) => (chosen, new), (None, _) => return Ok(None), }; - // TODO Step 12, set up browsing context features. + // TODO Step 15.2, Set up browsing context features for targetNavigable's + // active browsing context given tokenizedFeatures. let target_document = match chosen.document() { Some(target_document) => target_document, None => return Ok(None), @@ -496,7 +510,7 @@ impl WindowProxy { false }; let target_window = target_document.window(); - // Step 13, and 14.4, will have happened elsewhere, + // Step 15.3 and 15.4 will have happened elsewhere, // since we've created a new browsing context and loaded it with about:blank. if !url.is_empty() { let existing_document = self @@ -504,18 +518,18 @@ impl WindowProxy { .get() .and_then(ScriptThread::find_document) .unwrap(); - // Step 14.1 let url = match existing_document.url().join(&url) { Ok(url) => url, Err(_) => return Err(Error::Syntax), }; - // Step 14.3 let referrer = if noreferrer { Referrer::NoReferrer } else { target_window.as_global_scope().get_referrer() }; - // Step 14.5 + // Step 15.5 Otherwise, navigate targetNavigable to urlRecord using sourceDocument, + // with referrerPolicy set to referrerPolicy and exceptionsEnabled set to true. + // FIXME: referrerPolicy may not be used properly here. exceptionsEnabled not used. let referrer_policy = target_document.get_referrer_policy(); let pipeline_id = target_window.pipeline_id(); let secure = target_window.as_global_scope().is_secure_context(); @@ -534,14 +548,13 @@ impl WindowProxy { } else { NavigationHistoryBehavior::Push }; - target_window.load_url(history_handling, false, load_data, can_gc); } + // Step 17 (Dis-owning has been done in create_auxiliary_browsing_context). if noopener { - // Step 15 (Dis-owning has been done in create_auxiliary_browsing_context). return Ok(None); } - // Step 17. + // Step 18 Ok(target_document.browsing_context()) } diff --git a/components/script/dom/workerglobalscope.rs b/components/script/dom/workerglobalscope.rs index fa94dcc1d04..7912e90cdcf 100644 --- a/components/script/dom/workerglobalscope.rs +++ b/components/script/dom/workerglobalscope.rs @@ -446,7 +446,7 @@ impl WorkerGlobalScopeMethods<crate::DomTypeHolder> for WorkerGlobalScope { .queue_function_as_microtask(callback); } - // https://html.spec.whatwg.org/multipage/#dom-createimagebitmap + /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> fn CreateImageBitmap( &self, image: ImageBitmapSource, @@ -455,7 +455,30 @@ impl WorkerGlobalScopeMethods<crate::DomTypeHolder> for WorkerGlobalScope { ) -> Rc<Promise> { let p = self .upcast::<GlobalScope>() - .create_image_bitmap(image, options, can_gc); + .create_image_bitmap(image, 0, 0, None, None, options, can_gc); + p + } + + /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> + fn CreateImageBitmap_( + &self, + image: ImageBitmapSource, + sx: i32, + sy: i32, + sw: i32, + sh: i32, + options: &ImageBitmapOptions, + can_gc: CanGc, + ) -> Rc<Promise> { + let p = self.upcast::<GlobalScope>().create_image_bitmap( + image, + sx, + sy, + Some(sw), + Some(sh), + options, + can_gc, + ); p } diff --git a/components/script/dom/xmlhttprequest.rs b/components/script/dom/xmlhttprequest.rs index 4e7c136f42b..858cd24d45d 100644 --- a/components/script/dom/xmlhttprequest.rs +++ b/components/script/dom/xmlhttprequest.rs @@ -797,6 +797,7 @@ impl XMLHttpRequestMethods<crate::DomTypeHolder> for XMLHttpRequest { if self.ready_state.get() == XMLHttpRequestState::Done { self.change_ready_state(XMLHttpRequestState::Unsent, can_gc); self.response_status.set(Err(())); + *self.status.borrow_mut() = HttpStatus::new_error(); self.response.borrow_mut().clear(); self.response_headers.borrow_mut().clear(); } @@ -1188,6 +1189,8 @@ impl XMLHttpRequest { self.discard_subsequent_responses(); self.send_flag.set(false); + *self.status.borrow_mut() = HttpStatus::new_error(); + self.response_headers.borrow_mut().clear(); // XXXManishearth set response to NetworkError self.change_ready_state(XMLHttpRequestState::Done, can_gc); return_if_fetch_was_terminated!(); diff --git a/components/script/dom/xpathresult.rs b/components/script/dom/xpathresult.rs index f4a9ae7a31b..890e559ba97 100644 --- a/components/script/dom/xpathresult.rs +++ b/components/script/dom/xpathresult.rs @@ -54,7 +54,7 @@ impl TryFrom<u16> for XPathResultType { } } -#[derive(JSTraceable, MallocSizeOf)] +#[derive(Debug, JSTraceable, MallocSizeOf)] pub(crate) enum XPathResultValue { Boolean(bool), /// A IEEE-754 double-precision floating point number diff --git a/components/script/layout_dom/element.rs b/components/script/layout_dom/element.rs index 9b50c9f3a2b..fd34d591f0c 100644 --- a/components/script/layout_dom/element.rs +++ b/components/script/layout_dom/element.rs @@ -216,11 +216,15 @@ impl<'dom> style::dom::TElement for ServoLayoutElement<'dom> { } fn has_part_attr(&self) -> bool { - false + self.element + .get_attr_for_layout(&ns!(), &local_name!("part")) + .is_some() } fn exports_any_part(&self) -> bool { - false + self.element + .get_attr_for_layout(&ns!(), &local_name!("exportparts")) + .is_some() } fn style_attribute(&self) -> Option<ArcBorrow<StyleLocked<PropertyDeclarationBlock>>> { @@ -292,6 +296,17 @@ impl<'dom> style::dom::TElement for ServoLayoutElement<'dom> { } } + fn each_part<F>(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + if let Some(parts) = self.element.get_parts_for_layout() { + for part in parts { + callback(AtomIdent::cast(part)) + } + } + } + fn has_dirty_descendants(&self) -> bool { unsafe { self.as_node() @@ -728,8 +743,12 @@ impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> { } #[inline] - fn is_part(&self, _name: &AtomIdent) -> bool { - false + fn is_part(&self, name: &AtomIdent) -> bool { + self.element.has_class_or_part_for_layout( + name, + &local_name!("part"), + CaseSensitivity::CaseSensitive, + ) } fn imported_part(&self, _: &AtomIdent) -> Option<AtomIdent> { @@ -738,7 +757,8 @@ impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> { #[inline] fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { - self.element.has_class_for_layout(name, case_sensitivity) + self.element + .has_class_or_part_for_layout(name, &local_name!("class"), case_sensitivity) } fn is_html_slot_element(&self) -> bool { diff --git a/components/script/layout_dom/node.rs b/components/script/layout_dom/node.rs index dfb921b1ded..85b75f0b15f 100644 --- a/components/script/layout_dom/node.rs +++ b/components/script/layout_dom/node.rs @@ -119,9 +119,7 @@ impl<'dom> style::dom::TNode for ServoLayoutNode<'dom> { type ConcreteShadowRoot = ServoShadowRoot<'dom>; fn parent_node(&self) -> Option<Self> { - self.node - .composed_parent_node_ref() - .map(Self::from_layout_js) + self.node.parent_node_ref().map(Self::from_layout_js) } fn first_child(&self) -> Option<Self> { @@ -302,8 +300,8 @@ impl<'dom> ThreadSafeLayoutNode<'dom> for ServoThreadSafeLayoutNode<'dom> { } fn parent_style(&self) -> Arc<ComputedValues> { - let parent = self.node.parent_node().unwrap().as_element().unwrap(); - let parent_data = parent.borrow_data().unwrap(); + let parent_element = self.node.traversal_parent().unwrap(); + let parent_data = parent_element.borrow_data().unwrap(); parent_data.styles.primary().clone() } diff --git a/components/script/script_module.rs b/components/script/script_module.rs index 449f17901ed..2da7718a12a 100644 --- a/components/script/script_module.rs +++ b/components/script/script_module.rs @@ -43,7 +43,6 @@ use net_traits::{ ResourceFetchTiming, ResourceTimingType, }; use servo_url::ServoUrl; -use url::ParseError as UrlParseError; use uuid::Uuid; use crate::document_loader::LoadType; @@ -634,7 +633,7 @@ impl ModuleTree { specifier.handle().into_handle(), ); - if url.is_err() { + if url.is_none() { let specifier_error = gen_type_error(global, "Wrong module specifier".to_owned(), can_gc); @@ -660,24 +659,28 @@ impl ModuleTree { cx: *mut JSContext, url: &ServoUrl, specifier: RawHandle<*mut JSString>, - ) -> Result<ServoUrl, UrlParseError> { + ) -> Option<ServoUrl> { let specifier_str = unsafe { jsstring_to_str(cx, ptr::NonNull::new(*specifier).unwrap()) }; - // Step 1. - if let Ok(specifier_url) = ServoUrl::parse(&specifier_str) { - return Ok(specifier_url); - } + // TODO: We return the url here to keep the origianl behavior. should fix when we implement the full spec. + // Step 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL. + Self::resolve_url_like_module_specifier(&specifier_str, url) + } - // Step 2. - if !specifier_str.starts_with('/') && - !specifier_str.starts_with("./") && - !specifier_str.starts_with("../") + /// <https://html.spec.whatwg.org/multipage/#resolving-a-url-like-module-specifier> + fn resolve_url_like_module_specifier( + specifier: &DOMString, + base_url: &ServoUrl, + ) -> Option<ServoUrl> { + // Step 1. If specifier starts with "/", "./", or "../", then: + if specifier.starts_with('/') || specifier.starts_with("./") || specifier.starts_with("../") { - return Err(UrlParseError::InvalidDomainCharacter); + // Step 1.1. Let url be the result of URL parsing specifier with baseURL. + return ServoUrl::parse_with_base(Some(base_url), specifier).ok(); } - // Step 3. - ServoUrl::parse_with_base(Some(url), &specifier_str) + // Step 2. Let url be the result of URL parsing specifier (with no base URL). + ServoUrl::parse_with_base(None, specifier).ok() } /// <https://html.spec.whatwg.org/multipage/#finding-the-first-parse-error> @@ -1437,7 +1440,7 @@ fn fetch_an_import_module_script_graph( let url = ModuleTree::resolve_module_specifier(*cx, &base_url, specifier.handle().into()); // Step 2. - if url.is_err() { + if url.is_none() { let specifier_error = gen_type_error(global, "Wrong module specifier".to_owned(), can_gc); return Err(specifier_error); } @@ -1514,7 +1517,7 @@ unsafe extern "C" fn HostResolveImportedModule( ); // Step 6. - assert!(url.is_ok()); + assert!(url.is_some()); let parsed_url = url.unwrap(); diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index e138e7114d3..1260fa9867a 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -194,6 +194,8 @@ pub(crate) struct IncompleteParserContexts(RefCell<Vec<(PipelineId, ParserContex unsafe_no_jsmanaged_fields!(TaskQueue<MainThreadScriptMsg>); +type NodeIdSet = HashSet<String>; + #[derive(JSTraceable)] // ScriptThread instances are rooted on creation, so this is okay #[cfg_attr(crown, allow(crown::unrooted_must_root))] @@ -315,8 +317,9 @@ pub struct ScriptThread { #[no_trace] player_context: WindowGLContext, - /// A set of all nodes ever created in this script thread - node_ids: DomRefCell<HashSet<String>>, + /// A map from pipelines to all owned nodes ever created in this script thread + #[no_trace] + pipeline_to_node_ids: DomRefCell<HashMap<PipelineId, NodeIdSet>>, /// Code is running as a consequence of a user interaction is_user_interacting: Cell<bool>, @@ -818,14 +821,25 @@ impl ScriptThread { }) } - pub(crate) fn save_node_id(node_id: String) { + pub(crate) fn save_node_id(pipeline: PipelineId, node_id: String) { with_script_thread(|script_thread| { - script_thread.node_ids.borrow_mut().insert(node_id); + script_thread + .pipeline_to_node_ids + .borrow_mut() + .entry(pipeline) + .or_default() + .insert(node_id); }) } - pub(crate) fn has_node_id(node_id: &str) -> bool { - with_script_thread(|script_thread| script_thread.node_ids.borrow().contains(node_id)) + pub(crate) fn has_node_id(pipeline: PipelineId, node_id: &str) -> bool { + with_script_thread(|script_thread| { + script_thread + .pipeline_to_node_ids + .borrow() + .get(&pipeline) + .is_some_and(|node_ids| node_ids.contains(node_id)) + }) } /// Creates a new script thread. @@ -945,7 +959,7 @@ impl ScriptThread { unminify_css: opts.unminify_css, user_content_manager: state.user_content_manager, player_context: state.player_context, - node_ids: Default::default(), + pipeline_to_node_ids: Default::default(), is_user_interacting: Cell::new(false), #[cfg(feature = "webgpu")] gpu_id_hub: Arc::new(IdentityHub::default()), @@ -2266,13 +2280,12 @@ impl ScriptThread { can_gc, ) }, - WebDriverScriptCommand::FocusElement(element_id, reply) => { - webdriver_handlers::handle_focus_element( + WebDriverScriptCommand::GetElementShadowRoot(element_id, reply) => { + webdriver_handlers::handle_get_element_shadow_root( &documents, pipeline_id, element_id, reply, - can_gc, ) }, WebDriverScriptCommand::ElementClick(element_id, reply) => { @@ -2380,6 +2393,20 @@ impl ScriptThread { WebDriverScriptCommand::GetTitle(reply) => { webdriver_handlers::handle_get_title(&documents, pipeline_id, reply) }, + WebDriverScriptCommand::WillSendKeys( + element_id, + text, + strict_file_interactability, + reply, + ) => webdriver_handlers::handle_will_send_keys( + &documents, + pipeline_id, + element_id, + text, + strict_file_interactability, + reply, + can_gc, + ), _ => (), } } diff --git a/components/script/security_manager.rs b/components/script/security_manager.rs index ee062594eb8..89a8ba6ed89 100644 --- a/components/script/security_manager.rs +++ b/components/script/security_manager.rs @@ -2,7 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use net_traits::request::Referrer; +use std::sync::{Arc, Mutex}; + +use content_security_policy as csp; +use headers::{ContentType, HeaderMap, HeaderMapExt}; +use net_traits::request::{ + CredentialsMode, Referrer, RequestBody, RequestId, create_request_body_with_content, +}; +use net_traits::{ + FetchMetadata, FetchResponseListener, NetworkError, ResourceFetchTiming, ResourceTimingType, +}; use serde::Serialize; use servo_url::ServoUrl; use stylo_atoms::Atom; @@ -14,10 +23,14 @@ use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding }; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; +use crate::dom::bindings::root::DomRoot; use crate::dom::event::{Event, EventBubbles, EventCancelable, EventComposed}; use crate::dom::eventtarget::EventTarget; +use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::securitypolicyviolationevent::SecurityPolicyViolationEvent; use crate::dom::types::GlobalScope; +use crate::fetch::create_a_potential_cors_request; +use crate::network_listener::{PreInvoke, ResourceTimingListener, submit_timing}; use crate::script_runtime::CanGc; use crate::task::TaskOnce; @@ -25,9 +38,10 @@ pub(crate) struct CSPViolationReportTask { global: Trusted<GlobalScope>, event_target: Trusted<EventTarget>, violation_report: SecurityPolicyViolationReport, + violation_policy: csp::Policy, } -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SecurityPolicyViolationReport { sample: Option<String>, @@ -47,6 +61,30 @@ pub(crate) struct SecurityPolicyViolationReport { disposition: SecurityPolicyViolationEventDisposition, } +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +struct CSPReportUriViolationReportBody { + document_uri: String, + referrer: String, + blocked_uri: String, + effective_directive: String, + violated_directive: String, + original_policy: String, + #[serde(serialize_with = "serialize_disposition")] + disposition: SecurityPolicyViolationEventDisposition, + status_code: u16, + script_sample: Option<String>, + source_file: Option<String>, + line_number: Option<u32>, + column_number: Option<u32>, +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +struct CSPReportUriViolationReport { + csp_report: CSPReportUriViolationReportBody, +} + #[derive(Default)] pub(crate) struct CSPViolationReportBuilder { pub report_only: bool, @@ -114,36 +152,19 @@ impl CSPViolationReportBuilder { self } - /// <https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports> - fn strip_url_for_reports(&self, mut url: ServoUrl) -> String { - let scheme = url.scheme(); - // > Step 1: If url’s scheme is not an HTTP(S) scheme, then return url’s scheme. - if scheme != "https" && scheme != "http" { - return scheme.to_owned(); - } - // > Step 2: Set url’s fragment to the empty string. - url.set_fragment(None); - // > Step 3: Set url’s username to the empty string. - let _ = url.set_username(""); - // > Step 4: Set url’s password to the empty string. - let _ = url.set_password(None); - // > Step 5: Return the result of executing the URL serializer on url. - url.into_string() - } - pub fn build(self, global: &GlobalScope) -> SecurityPolicyViolationReport { SecurityPolicyViolationReport { violated_directive: self.effective_directive.clone(), effective_directive: self.effective_directive.clone(), - document_url: self.strip_url_for_reports(global.get_url()), + document_url: strip_url_for_reports(global.get_url()), disposition: match self.report_only { true => SecurityPolicyViolationEventDisposition::Report, false => SecurityPolicyViolationEventDisposition::Enforce, }, // https://w3c.github.io/webappsec-csp/#violation-referrer referrer: match global.get_referrer() { - Referrer::Client(url) => self.strip_url_for_reports(url), - Referrer::ReferrerUrl(url) => self.strip_url_for_reports(url), + Referrer::Client(url) => strip_url_for_reports(url), + Referrer::ReferrerUrl(url) => strip_url_for_reports(url), _ => "".to_owned(), }, sample: self.sample, @@ -162,22 +183,24 @@ impl CSPViolationReportTask { global: Trusted<GlobalScope>, event_target: Trusted<EventTarget>, violation_report: SecurityPolicyViolationReport, + violation_policy: csp::Policy, ) -> CSPViolationReportTask { CSPViolationReportTask { global, event_target, violation_report, + violation_policy, } } - fn fire_violation_event(self, can_gc: CanGc) { + fn fire_violation_event(&self, can_gc: CanGc) { let event = SecurityPolicyViolationEvent::new( &self.global.root(), Atom::from("securitypolicyviolation"), EventBubbles::Bubbles, EventCancelable::Cancelable, EventComposed::Composed, - &self.violation_report.convert(), + &self.violation_report.clone().convert(), can_gc, ); @@ -185,6 +208,72 @@ impl CSPViolationReportTask { .upcast::<Event>() .fire(&self.event_target.root(), can_gc); } + + /// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation> + fn serialize_violation(&self) -> Option<RequestBody> { + let report_body = CSPReportUriViolationReport { + // Steps 1-3. + csp_report: self.violation_report.clone().into(), + }; + // Step 4. Return the result of serialize an infra value to JSON bytes given «[ "csp-report" → body ]». + Some(create_request_body_with_content( + &serde_json::to_string(&report_body).unwrap_or("".to_owned()), + )) + } + + /// Step 3.4 of <https://www.w3.org/TR/CSP/#report-violation> + fn post_csp_violation_to_report_uri(&self, report_uri_directive: &csp::Directive) { + let global = self.global.root(); + // Step 3.4.1. If violation’s policy’s directive set contains a directive named + // "report-to", skip the remaining substeps. + if self + .violation_policy + .contains_a_directive_whose_name_is("report-to") + { + return; + } + // Step 3.4.2. For each token of directive’s value: + for token in &report_uri_directive.value { + // Step 3.4.2.1. Let endpoint be the result of executing the URL parser with token as the input, + // and violation’s url as the base URL. + let Ok(endpoint) = ServoUrl::parse_with_base(Some(&global.get_url()), token) else { + // Step 3.4.2.2. If endpoint is not a valid URL, skip the remaining substeps. + continue; + }; + // Step 3.4.2.3. Let request be a new request, initialized as follows: + let mut headers = HeaderMap::with_capacity(1); + headers.typed_insert(ContentType::from( + "application/csp-report".parse::<mime::Mime>().unwrap(), + )); + let request_body = self.serialize_violation(); + let request = create_a_potential_cors_request( + None, + endpoint.clone(), + csp::Destination::Report, + None, + None, + global.get_referrer(), + global.insecure_requests_policy(), + global.has_trustworthy_ancestor_or_current_origin(), + global.policy_container(), + ) + .method(http::Method::POST) + .body(request_body) + .origin(global.origin().immutable().clone()) + .credentials_mode(CredentialsMode::CredentialsSameOrigin) + .headers(headers); + // Step 3.4.2.4. Fetch request. The result will be ignored. + global.fetch( + request, + Arc::new(Mutex::new(CSPReportUriFetchListener { + endpoint, + global: Trusted::new(&global), + resource_timing: ResourceFetchTiming::new(ResourceTimingType::None), + })), + global.task_manager().networking_task_source().into(), + ); + } + } } /// Corresponds to the operation in 5.5 Report Violation @@ -196,7 +285,15 @@ impl TaskOnce for CSPViolationReportTask { // > that uses the SecurityPolicyViolationEvent interface // > at target with its attributes initialized as follows: self.fire_violation_event(CanGc::note()); - // TODO: Support `report-to` directive that corresponds to 5.5.3.5. + // Step 3.4. If violation’s policy’s directive set contains a directive named "report-uri" directive: + if let Some(report_uri_directive) = self + .violation_policy + .directive_set + .iter() + .find(|directive| directive.name == "report-uri") + { + self.post_csp_violation_to_report_uri(report_uri_directive); + } } } @@ -220,6 +317,62 @@ impl Convert<SecurityPolicyViolationEventInit> for SecurityPolicyViolationReport } } +/// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation> +impl From<SecurityPolicyViolationReport> for CSPReportUriViolationReportBody { + fn from(value: SecurityPolicyViolationReport) -> Self { + // Step 1. Let body be a map with its keys initialized as follows: + let mut converted = Self { + document_uri: value.document_url, + referrer: value.referrer, + blocked_uri: value.blocked_url, + effective_directive: value.effective_directive, + violated_directive: value.violated_directive, + original_policy: value.original_policy, + disposition: value.disposition, + status_code: value.status_code, + script_sample: None, + source_file: None, + line_number: None, + column_number: None, + }; + + // Step 2. If violation’s source file is not null: + if !value.source_file.is_empty() { + // Step 2.1. Set body["source-file'] to the result of + // executing § 5.4 Strip URL for use in reports on violation’s source file. + converted.source_file = ServoUrl::parse(&value.source_file) + .map(strip_url_for_reports) + .ok(); + // Step 2.2. Set body["line-number"] to violation’s line number. + converted.line_number = Some(value.line_number); + // Step 2.3. Set body["column-number"] to violation’s column number. + converted.column_number = Some(value.column_number); + } + + // Step 3. Assert: If body["blocked-uri"] is not "inline", then body["sample"] is the empty string. + debug_assert!(converted.blocked_uri == "inline" || converted.script_sample.is_none()); + + converted + } +} + +/// <https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports> +fn strip_url_for_reports(mut url: ServoUrl) -> String { + let scheme = url.scheme(); + // > Step 1: If url’s scheme is not an HTTP(S) scheme, then return url’s scheme. + if scheme != "https" && scheme != "http" { + return scheme.to_owned(); + } + // > Step 2: Set url’s fragment to the empty string. + url.set_fragment(None); + // > Step 3: Set url’s username to the empty string. + let _ = url.set_username(""); + // > Step 4: Set url’s password to the empty string. + let _ = url.set_password(None); + // > Step 5: Return the result of executing the URL serializer on url. + url.into_string() +} + fn serialize_disposition<S: serde::Serializer>( val: &SecurityPolicyViolationEventDisposition, serializer: S, @@ -229,3 +382,71 @@ fn serialize_disposition<S: serde::Serializer>( SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"), } } + +struct CSPReportUriFetchListener { + /// Endpoint URL of this request. + endpoint: ServoUrl, + /// Timing data for this resource. + resource_timing: ResourceFetchTiming, + /// The global object fetching the report uri violation + global: Trusted<GlobalScope>, +} + +impl FetchResponseListener for CSPReportUriFetchListener { + fn process_request_body(&mut self, _: RequestId) {} + + fn process_request_eof(&mut self, _: RequestId) {} + + fn process_response( + &mut self, + _: RequestId, + fetch_metadata: Result<FetchMetadata, NetworkError>, + ) { + _ = fetch_metadata; + } + + fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) { + _ = chunk; + } + + fn process_response_eof( + &mut self, + _: RequestId, + response: Result<ResourceFetchTiming, NetworkError>, + ) { + _ = response; + } + + fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming { + &mut self.resource_timing + } + + fn resource_timing(&self) -> &ResourceFetchTiming { + &self.resource_timing + } + + fn submit_resource_timing(&mut self) { + submit_timing(self, CanGc::note()) + } + + fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<csp::Violation>) { + let global = &self.resource_timing_global(); + global.report_csp_violations(violations, None); + } +} + +impl ResourceTimingListener for CSPReportUriFetchListener { + fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { + (InitiatorType::Other, self.endpoint.clone()) + } + + fn resource_timing_global(&self) -> DomRoot<GlobalScope> { + self.global.root() + } +} + +impl PreInvoke for CSPReportUriFetchListener { + fn should_invoke(&self) -> bool { + true + } +} diff --git a/components/script/textinput.rs b/components/script/textinput.rs index 73f6159a9a0..6213b1ef511 100644 --- a/components/script/textinput.rs +++ b/components/script/textinput.rs @@ -998,6 +998,9 @@ impl<T: ClipboardProvider> TextInput<T> { self.insert_string(c.as_str()); return KeyReaction::DispatchInput; } + if matches!(key, Key::Process) { + return KeyReaction::DispatchInput; + } KeyReaction::Nothing }) .unwrap() diff --git a/components/script/timers.rs b/components/script/timers.rs index 0dc1397fbdd..fe542c09a56 100644 --- a/components/script/timers.rs +++ b/components/script/timers.rs @@ -4,7 +4,7 @@ use std::cell::Cell; use std::cmp::{Ord, Ordering}; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::default::Default; use std::rc::Rc; use std::time::{Duration, Instant}; @@ -47,7 +47,7 @@ pub(crate) struct OneshotTimers { global_scope: Dom<GlobalScope>, js_timers: JsTimers, next_timer_handle: Cell<OneshotTimerHandle>, - timers: DomRefCell<Vec<OneshotTimer>>, + timers: DomRefCell<VecDeque<OneshotTimer>>, suspended_since: Cell<Option<Instant>>, /// Initially 0, increased whenever the associated document is reactivated /// by the amount of ms the document was inactive. The current time can be @@ -131,7 +131,7 @@ impl OneshotTimers { global_scope: Dom::from_ref(global_scope), js_timers: JsTimers::default(), next_timer_handle: Cell::new(OneshotTimerHandle(1)), - timers: DomRefCell::new(Vec::new()), + timers: DomRefCell::new(VecDeque::new()), suspended_since: Cell::new(None), suspension_offset: Cell::new(Duration::ZERO), expected_event_id: Cell::new(TimerEventId(0)), @@ -180,7 +180,7 @@ impl OneshotTimers { } fn is_next_timer(&self, handle: OneshotTimerHandle) -> bool { - match self.timers.borrow().last() { + match self.timers.borrow().back() { None => false, Some(max_timer) => max_timer.handle == handle, } @@ -201,7 +201,7 @@ impl OneshotTimers { let base_time = self.base_time(); // Since the event id was the expected one, at least one timer should be due. - if base_time < self.timers.borrow().last().unwrap().scheduled_for { + if base_time < self.timers.borrow().back().unwrap().scheduled_for { warn!("Unexpected timing!"); return; } @@ -213,11 +213,11 @@ impl OneshotTimers { loop { let mut timers = self.timers.borrow_mut(); - if timers.is_empty() || timers.last().unwrap().scheduled_for > base_time { + if timers.is_empty() || timers.back().unwrap().scheduled_for > base_time { break; } - timers_to_run.push(timers.pop().unwrap()); + timers_to_run.push(timers.pop_back().unwrap()); } for timer in timers_to_run { @@ -286,7 +286,7 @@ impl OneshotTimers { } let timers = self.timers.borrow(); - let Some(timer) = timers.last() else { + let Some(timer) = timers.back() else { return; }; diff --git a/components/script/webdriver_handlers.rs b/components/script/webdriver_handlers.rs index 6b4264d945e..1bde6f314f2 100644 --- a/components/script/webdriver_handlers.rs +++ b/components/script/webdriver_handlers.rs @@ -9,9 +9,7 @@ use std::ptr::NonNull; use base::id::{BrowsingContextId, PipelineId}; use cookie::Cookie; -use embedder_traits::{ - WebDriverCookieError, WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverJSValue, -}; +use embedder_traits::{WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverJSValue}; use euclid::default::{Point2D, Rect, Size2D}; use hyper_serde::Serde; use ipc_channel::ipc::{self, IpcSender}; @@ -81,7 +79,9 @@ fn find_node_by_unique_id( match documents.find_document(pipeline) { Some(doc) => find_node_by_unique_id_in_document(&doc, node_id), None => { - if ScriptThread::has_node_id(&node_id) { + // FIXME: This is unreacheable!! Because we already early return in Constellation + // To be Fixed soon + if ScriptThread::has_node_id(pipeline, &node_id) { Err(ErrorStatus::StaleElementReference) } else { Err(ErrorStatus::NoSuchElement) @@ -94,14 +94,15 @@ pub(crate) fn find_node_by_unique_id_in_document( document: &Document, node_id: String, ) -> Result<DomRoot<Node>, ErrorStatus> { + let pipeline = document.window().pipeline_id(); match document .upcast::<Node>() .traverse_preorder(ShadowIncluding::Yes) - .find(|node| node.unique_id() == node_id) + .find(|node| node.unique_id(pipeline) == node_id) { Some(node) => Ok(node), None => { - if ScriptThread::has_node_id(&node_id) { + if ScriptThread::has_node_id(pipeline, &node_id) { Err(ErrorStatus::StaleElementReference) } else { Err(ErrorStatus::NoSuchElement) @@ -129,7 +130,10 @@ fn matching_links( content == link_text } }) - .map(|node| node.upcast::<Node>().unique_id()) + .map(|node| { + node.upcast::<Node>() + .unique_id(node.owner_doc().window().pipeline_id()) + }) } fn all_matching_links( @@ -329,20 +333,25 @@ unsafe fn jsval_to_webdriver_inner( Ok(WebDriverJSValue::ArrayLike(result)) } else if let Ok(element) = root_from_object::<Element>(*object, cx) { Ok(WebDriverJSValue::Element(WebElement( - element.upcast::<Node>().unique_id(), + element + .upcast::<Node>() + .unique_id(element.owner_document().window().pipeline_id()), ))) } else if let Ok(window) = root_from_object::<Window>(*object, cx) { let window_proxy = window.window_proxy(); if window_proxy.is_browsing_context_discarded() { return Err(WebDriverJSError::StaleElementReference); - } else if window_proxy.browsing_context_id() == window_proxy.webview_id() { - Ok(WebDriverJSValue::Window(WebWindow( - window.Document().upcast::<Node>().unique_id(), - ))) } else { - Ok(WebDriverJSValue::Frame(WebFrame( - window.Document().upcast::<Node>().unique_id(), - ))) + let pipeline = window.pipeline_id(); + if window_proxy.browsing_context_id() == window_proxy.webview_id() { + Ok(WebDriverJSValue::Window(WebWindow( + window.Document().upcast::<Node>().unique_id(pipeline), + ))) + } else { + Ok(WebDriverJSValue::Frame(WebFrame( + window.Document().upcast::<Node>().unique_id(pipeline), + ))) + } } } else if object_has_to_json_property(cx, global_scope, object.handle()) { let name = CString::new("toJSON").unwrap(); @@ -598,7 +607,7 @@ pub(crate) fn handle_find_element_css( .QuerySelector(DOMString::from(selector)) .map_err(|_| ErrorStatus::InvalidSelector) }) - .map(|node| node.map(|x| x.upcast::<Node>().unique_id())), + .map(|node| node.map(|x| x.upcast::<Node>().unique_id(pipeline))), ) .unwrap(); } @@ -640,7 +649,7 @@ pub(crate) fn handle_find_element_tag_name( .elements_iter() .next() }) - .map(|node| node.map(|x| x.upcast::<Node>().unique_id())), + .map(|node| node.map(|x| x.upcast::<Node>().unique_id(pipeline))), ) .unwrap(); } @@ -664,7 +673,7 @@ pub(crate) fn handle_find_elements_css( .map(|nodes| { nodes .iter() - .map(|x| x.upcast::<Node>().unique_id()) + .map(|x| x.upcast::<Node>().unique_id(pipeline)) .collect() }), ) @@ -706,7 +715,7 @@ pub(crate) fn handle_find_elements_tag_name( .map(|nodes| { nodes .elements_iter() - .map(|x| x.upcast::<Node>().unique_id()) + .map(|x| x.upcast::<Node>().unique_id(pipeline)) .collect::<Vec<String>>() }), ) @@ -725,7 +734,7 @@ pub(crate) fn handle_find_element_element_css( find_node_by_unique_id(documents, pipeline, element_id).and_then(|node| { node.query_selector(DOMString::from(selector)) .map_err(|_| ErrorStatus::InvalidSelector) - .map(|node| node.map(|x| x.upcast::<Node>().unique_id())) + .map(|node| node.map(|x| x.upcast::<Node>().unique_id(pipeline))) }), ) .unwrap(); @@ -764,7 +773,7 @@ pub(crate) fn handle_find_element_element_tag_name( .GetElementsByTagName(DOMString::from(selector), can_gc) .elements_iter() .next() - .map(|x| x.upcast::<Node>().unique_id())), + .map(|x| x.upcast::<Node>().unique_id(pipeline))), None => Err(ErrorStatus::UnknownError), }), ) @@ -786,7 +795,7 @@ pub(crate) fn handle_find_element_elements_css( .map(|nodes| { nodes .iter() - .map(|x| x.upcast::<Node>().unique_id()) + .map(|x| x.upcast::<Node>().unique_id(pipeline)) .collect() }) }), @@ -826,7 +835,7 @@ pub(crate) fn handle_find_element_elements_tag_name( Some(element) => Ok(element .GetElementsByTagName(DOMString::from(selector), can_gc) .elements_iter() - .map(|x| x.upcast::<Node>().unique_id()) + .map(|x| x.upcast::<Node>().unique_id(pipeline)) .collect::<Vec<String>>()), None => Err(ErrorStatus::UnknownError), }), @@ -834,24 +843,87 @@ pub(crate) fn handle_find_element_elements_tag_name( .unwrap(); } -pub(crate) fn handle_focus_element( +/// <https://www.w3.org/TR/webdriver2/#dfn-get-element-shadow-root> +pub(crate) fn handle_get_element_shadow_root( documents: &DocumentCollection, pipeline: PipelineId, element_id: String, - reply: IpcSender<Result<(), ErrorStatus>>, + reply: IpcSender<Result<Option<String>, ErrorStatus>>, +) { + reply + .send( + find_node_by_unique_id(documents, pipeline, element_id).and_then(|node| match node + .downcast::<Element>( + ) { + Some(element) => Ok(element + .GetShadowRoot() + .map(|x| x.upcast::<Node>().unique_id(pipeline))), + None => Err(ErrorStatus::NoSuchElement), + }), + ) + .unwrap(); +} + +pub(crate) fn handle_will_send_keys( + documents: &DocumentCollection, + pipeline: PipelineId, + element_id: String, + text: String, + strict_file_interactability: bool, + reply: IpcSender<Result<bool, ErrorStatus>>, can_gc: CanGc, ) { reply .send( find_node_by_unique_id(documents, pipeline, element_id).and_then(|node| { - match node.downcast::<HTMLElement>() { - Some(element) => { - // Need a way to find if this actually succeeded - element.Focus(can_gc); - Ok(()) - }, - None => Err(ErrorStatus::UnknownError), + // Step 6: Let file be true if element is input element + // in the file upload state, or false otherwise + let file_input = node + .downcast::<HTMLInputElement>() + .filter(|&input_element| input_element.input_type() == InputType::File); + + // Step 7: If file is false or the session's strict file interactability + if file_input.is_none() || strict_file_interactability { + match node.downcast::<HTMLElement>() { + Some(element) => { + // Need a way to find if this actually succeeded + element.Focus(can_gc); + }, + None => return Err(ErrorStatus::UnknownError), + } } + + // Step 8 (file input) + if let Some(file_input) = file_input { + // Step 8.1: Let files be the result of splitting text + // on the newline (\n) character. + let files: Vec<DOMString> = text.split("\n").map(|s| s.into()).collect(); + + // Step 8.2 + if files.is_empty() { + return Err(ErrorStatus::InvalidArgument); + } + + // Step 8.3 - 8.4 + if !file_input.Multiple() && files.len() > 1 { + return Err(ErrorStatus::InvalidArgument); + } + + // Step 8.5 + // TODO: Should return invalid argument error if file doesn't exist + + // Step 8.6 - 8.7 + // Input and change event already fired in `htmlinputelement.rs`. + file_input.SelectFiles(files, can_gc); + + // Step 8.8 + return Ok(false); + } + + // TODO: Check non-typeable form control + // TODO: Check content editable + + Ok(true) }), ) .unwrap(); @@ -867,7 +939,7 @@ pub(crate) fn handle_get_active_element( documents .find_document(pipeline) .and_then(|document| document.GetActiveElement()) - .map(|element| element.upcast::<Node>().unique_id()), + .map(|element| element.upcast::<Node>().unique_id(pipeline)), ) .unwrap(); } @@ -922,7 +994,7 @@ pub(crate) fn handle_get_page_source( pub(crate) fn handle_get_cookies( documents: &DocumentCollection, pipeline: PipelineId, - reply: IpcSender<Vec<Serde<Cookie<'static>>>>, + reply: IpcSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>, ) { reply .send( @@ -936,9 +1008,9 @@ pub(crate) fn handle_get_cookies( .as_global_scope() .resource_threads() .send(GetCookiesDataForUrl(url, sender, NonHTTP)); - receiver.recv().unwrap() + Ok(receiver.recv().unwrap()) }, - None => Vec::new(), + None => Ok(Vec::new()), }, ) .unwrap(); @@ -949,7 +1021,7 @@ pub(crate) fn handle_get_cookie( documents: &DocumentCollection, pipeline: PipelineId, name: String, - reply: IpcSender<Vec<Serde<Cookie<'static>>>>, + reply: IpcSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>, ) { reply .send( @@ -964,12 +1036,12 @@ pub(crate) fn handle_get_cookie( .resource_threads() .send(GetCookiesDataForUrl(url, sender, NonHTTP)); let cookies = receiver.recv().unwrap(); - cookies + Ok(cookies .into_iter() .filter(|cookie| cookie.name() == &*name) - .collect() + .collect()) }, - None => Vec::new(), + None => Ok(Vec::new()), }, ) .unwrap(); @@ -980,15 +1052,13 @@ pub(crate) fn handle_add_cookie( documents: &DocumentCollection, pipeline: PipelineId, cookie: Cookie<'static>, - reply: IpcSender<Result<(), WebDriverCookieError>>, + reply: IpcSender<Result<(), ErrorStatus>>, ) { // TODO: Return a different error if the pipeline doesn't exist let document = match documents.find_document(pipeline) { Some(document) => document, None => { - return reply - .send(Err(WebDriverCookieError::UnableToSetCookie)) - .unwrap(); + return reply.send(Err(ErrorStatus::UnableToSetCookie)).unwrap(); }, }; let url = document.url(); @@ -1001,7 +1071,7 @@ pub(crate) fn handle_add_cookie( let domain = cookie.domain().map(ToOwned::to_owned); reply .send(match (document.is_cookie_averse(), domain) { - (true, _) => Err(WebDriverCookieError::InvalidDomain), + (true, _) => Err(ErrorStatus::InvalidCookieDomain), (false, Some(ref domain)) if url.host_str().map(|x| x == domain).unwrap_or(false) => { let _ = document .window() @@ -1018,7 +1088,7 @@ pub(crate) fn handle_add_cookie( .send(SetCookieForUrl(url, Serde(cookie), method)); Ok(()) }, - (_, _) => Err(WebDriverCookieError::UnableToSetCookie), + (_, _) => Err(ErrorStatus::UnableToSetCookie), }) .unwrap(); } @@ -1395,7 +1465,7 @@ pub(crate) fn handle_element_click( Ok(None) }, - None => Ok(Some(node.unique_id())), + None => Ok(Some(node.unique_id(pipeline))), } }), ) diff --git a/components/script/xpath/context.rs b/components/script/xpath/context.rs index df78a9a7bec..ff51c92521d 100644 --- a/components/script/xpath/context.rs +++ b/components/script/xpath/context.rs @@ -5,10 +5,14 @@ use std::iter::Enumerate; use std::vec::IntoIter; +use script_bindings::str::DOMString; + use super::Node; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::root::DomRoot; /// The context during evaluation of an XPath expression. +#[derive(Debug)] pub(crate) struct EvaluationCtx { /// Where we started at pub(crate) starting_node: DomRoot<Node>, @@ -20,7 +24,7 @@ pub(crate) struct EvaluationCtx { pub(crate) predicate_nodes: Option<Vec<DomRoot<Node>>>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub(crate) struct PredicateCtx { pub(crate) index: usize, pub(crate) size: usize, @@ -68,6 +72,12 @@ impl EvaluationCtx { size, } } + + /// Resolve a namespace prefix using the context node's document + pub(crate) fn resolve_namespace(&self, prefix: Option<&str>) -> Option<DOMString> { + self.context_node + .LookupNamespaceURI(prefix.map(DOMString::from)) + } } /// When evaluating predicates, we need to keep track of the current node being evaluated and diff --git a/components/script/xpath/eval.rs b/components/script/xpath/eval.rs index 75d7ac5849c..38d8c2fee90 100644 --- a/components/script/xpath/eval.rs +++ b/components/script/xpath/eval.rs @@ -12,6 +12,7 @@ use super::parser::{ QName as ParserQualName, RelationalOp, StepExpr, UnaryOp, }; use super::{EvaluationCtx, Value}; +use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::inheritance::{Castable, CharacterDataTypeId, NodeTypeId}; use crate::dom::bindings::root::DomRoot; @@ -83,6 +84,22 @@ where } } +impl<T> Evaluatable for Option<T> +where + T: Evaluatable, +{ + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + match self { + Some(expr) => expr.evaluate(context), + None => Ok(Value::Nodeset(vec![])), + } + } + + fn is_primitive(&self) -> bool { + self.as_ref().is_some_and(|t| T::is_primitive(t)) + } +} + impl Evaluatable for Expr { fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { match self { @@ -230,21 +247,31 @@ impl Evaluatable for PathExpr { } } -impl TryFrom<&ParserQualName> for QualName { +pub(crate) struct QualNameConverter<'a> { + qname: &'a ParserQualName, + context: &'a EvaluationCtx, +} + +impl<'a> TryFrom<QualNameConverter<'a>> for QualName { type Error = Error; - fn try_from(qname: &ParserQualName) -> Result<Self, Self::Error> { - let qname_as_str = qname.to_string(); - if let Ok((ns, prefix, local)) = validate_and_extract(None, &qname_as_str) { + fn try_from(converter: QualNameConverter<'a>) -> Result<Self, Self::Error> { + let qname_as_str = converter.qname.to_string(); + let namespace = converter + .context + .resolve_namespace(converter.qname.prefix.as_deref()); + + if let Ok((ns, prefix, local)) = validate_and_extract(namespace, &qname_as_str) { Ok(QualName { prefix, ns, local }) } else { Err(Error::InvalidQName { - qname: qname.clone(), + qname: converter.qname.clone(), }) } } } +#[derive(Debug)] pub(crate) enum NameTestComparisonMode { /// Namespaces must match exactly XHtml, @@ -294,29 +321,41 @@ pub(crate) fn element_name_test( } } -fn apply_node_test(test: &NodeTest, node: &Node) -> Result<bool, Error> { +fn apply_node_test(context: &EvaluationCtx, test: &NodeTest, node: &Node) -> Result<bool, Error> { let result = match test { NodeTest::Name(qname) => { // Convert the unvalidated "parser QualName" into the proper QualName structure - let wanted_name: QualName = qname.try_into()?; - if matches!(node.type_id(), NodeTypeId::Element(_)) { - let element = node.downcast::<Element>().unwrap(); - let comparison_mode = if node.owner_doc().is_xhtml_document() { - NameTestComparisonMode::XHtml - } else { - NameTestComparisonMode::Html - }; - let element_qualname = QualName::new( - element.prefix().as_ref().cloned(), - element.namespace().clone(), - element.local_name().clone(), - ); - element_name_test(wanted_name, element_qualname, comparison_mode) - } else { - false + let wanted_name: QualName = QualNameConverter { qname, context }.try_into()?; + match node.type_id() { + NodeTypeId::Element(_) => { + let element = node.downcast::<Element>().unwrap(); + let comparison_mode = if node.owner_doc().is_html_document() { + NameTestComparisonMode::Html + } else { + NameTestComparisonMode::XHtml + }; + let element_qualname = QualName::new( + element.prefix().as_ref().cloned(), + element.namespace().clone(), + element.local_name().clone(), + ); + element_name_test(wanted_name, element_qualname, comparison_mode) + }, + NodeTypeId::Attr => { + let attr = node.downcast::<Attr>().unwrap(); + let attr_qualname = QualName::new( + attr.prefix().cloned(), + attr.namespace().clone(), + attr.local_name().clone(), + ); + // attributes are always compared with strict namespace matching + let comparison_mode = NameTestComparisonMode::XHtml; + element_name_test(wanted_name, attr_qualname, comparison_mode) + }, + _ => false, } }, - NodeTest::Wildcard => true, + NodeTest::Wildcard => matches!(node.type_id(), NodeTypeId::Element(_)), NodeTest::Kind(kind) => match kind { KindTest::PI(target) => { if NodeTypeId::CharacterData(CharacterDataTypeId::ProcessingInstruction) == @@ -411,7 +450,7 @@ impl Evaluatable for StepExpr { let filtered_nodes: Vec<DomRoot<Node>> = nodes .into_iter() .map(|node| { - apply_node_test(&axis_step.node_test, &node) + apply_node_test(context, &axis_step.node_test, &node) .map(|matches| matches.then_some(node)) }) .collect::<Result<Vec<_>, _>>()? @@ -489,6 +528,7 @@ impl Evaluatable for PredicateExpr { let v = match eval_result { Ok(Value::Number(v)) => Ok(predicate_ctx.index == v as usize), + Ok(Value::Boolean(v)) => Ok(v), Ok(v) => Ok(v.boolean()), Err(e) => Err(e), }; diff --git a/components/script/xpath/eval_function.rs b/components/script/xpath/eval_function.rs index caf0782c07b..53c14944474 100644 --- a/components/script/xpath/eval_function.rs +++ b/components/script/xpath/eval_function.rs @@ -2,12 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use style::Atom; + use super::Value; use super::context::EvaluationCtx; use super::eval::{Error, Evaluatable, try_extract_nodeset}; use super::parser::CoreFunction; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::inheritance::{Castable, NodeTypeId}; +use crate::dom::bindings::root::DomRoot; use crate::dom::element::Element; use crate::dom::node::Node; @@ -101,6 +104,31 @@ pub(crate) fn normalize_space(s: &str) -> String { result } +/// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-lang> +fn lang_matches(context_lang: Option<&str>, target_lang: &str) -> bool { + let Some(context_lang) = context_lang else { + return false; + }; + + let context_lower = context_lang.to_ascii_lowercase(); + let target_lower = target_lang.to_ascii_lowercase(); + + if context_lower == target_lower { + return true; + } + + // Check if context is target with additional suffix + if context_lower.starts_with(&target_lower) { + // Make sure the next character is a hyphen to avoid matching + // e.g. "england" when target is "en" + if let Some(next_char) = context_lower.chars().nth(target_lower.len()) { + return next_char == '-'; + } + } + + false +} + impl Evaluatable for CoreFunction { fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { match self { @@ -131,7 +159,20 @@ impl Evaluatable for CoreFunction { .collect(); Ok(Value::String(strings?.join(""))) }, - CoreFunction::Id(_expr) => todo!(), + CoreFunction::Id(expr) => { + let args_str = expr.evaluate(context)?.string(); + let args_normalized = normalize_space(&args_str); + let args = args_normalized.split(' '); + + let document = context.context_node.owner_doc(); + let mut result = Vec::new(); + for arg in args { + for element in document.get_elements_with_id(&Atom::from(arg)).iter() { + result.push(DomRoot::from_ref(element.upcast::<Node>())); + } + } + Ok(Value::Nodeset(result)) + }, CoreFunction::LocalName(expr_opt) => { let node = match expr_opt { Some(expr) => expr @@ -256,7 +297,11 @@ impl Evaluatable for CoreFunction { CoreFunction::Not(expr) => Ok(Value::Boolean(!expr.evaluate(context)?.boolean())), CoreFunction::True => Ok(Value::Boolean(true)), CoreFunction::False => Ok(Value::Boolean(false)), - CoreFunction::Lang(_) => Ok(Value::Nodeset(vec![])), // Not commonly used in the DOM, short-circuit it + CoreFunction::Lang(expr) => { + let context_lang = context.context_node.get_lang(); + let lang = expr.evaluate(context)?.string(); + Ok(Value::Boolean(lang_matches(context_lang.as_deref(), &lang))) + }, } } @@ -319,7 +364,7 @@ impl Evaluatable for CoreFunction { } #[cfg(test)] mod tests { - use super::{substring, substring_after, substring_before}; + use super::{lang_matches, substring, substring_after, substring_before}; #[test] fn test_substring_before() { @@ -354,4 +399,18 @@ mod tests { assert_eq!(substring("hello", 0, Some(0)), ""); assert_eq!(substring("hello", 0, Some(-5)), ""); } + + #[test] + fn test_lang_matches() { + assert!(lang_matches(Some("en"), "en")); + assert!(lang_matches(Some("EN"), "en")); + assert!(lang_matches(Some("en"), "EN")); + assert!(lang_matches(Some("en-US"), "en")); + assert!(lang_matches(Some("en-GB"), "en")); + + assert!(!lang_matches(Some("eng"), "en")); + assert!(!lang_matches(Some("fr"), "en")); + assert!(!lang_matches(Some("fr-en"), "en")); + assert!(!lang_matches(None, "en")); + } } diff --git a/components/script/xpath/eval_value.rs b/components/script/xpath/eval_value.rs index de6c13e3454..66f1b92c6d4 100644 --- a/components/script/xpath/eval_value.rs +++ b/components/script/xpath/eval_value.rs @@ -8,7 +8,6 @@ use std::{fmt, string}; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::utils::AsVoidPtr; use crate::dom::node::Node; /// The primary types of values that an XPath expression returns as a result. @@ -216,7 +215,7 @@ impl NodesetHelpers for Vec<DomRoot<Node>> { } fn document_order(&self) -> Vec<DomRoot<Node>> { let mut nodes: Vec<DomRoot<Node>> = self.clone(); - if nodes.len() == 1 { + if nodes.len() <= 1 { return nodes; } @@ -233,10 +232,13 @@ impl NodesetHelpers for Vec<DomRoot<Node>> { nodes } fn document_order_unique(&self) -> Vec<DomRoot<Node>> { - let mut nodes: Vec<DomRoot<Node>> = self.document_order(); - - nodes.dedup_by_key(|n| n.as_void_ptr()); + let mut seen = HashSet::new(); + let unique_nodes: Vec<DomRoot<Node>> = self + .iter() + .filter(|node| seen.insert(node.to_opaque())) + .cloned() + .collect(); - nodes + unique_nodes.document_order() } } diff --git a/components/script/xpath/parser.rs b/components/script/xpath/parser.rs index b1a4bfcc42d..272fa41dcdf 100644 --- a/components/script/xpath/parser.rs +++ b/components/script/xpath/parser.rs @@ -510,40 +510,45 @@ fn union_expr(input: &str) -> IResult<&str, Expr> { fn path_expr(input: &str) -> IResult<&str, Expr> { alt(( // "//" RelativePathExpr - map(pair(tag("//"), relative_path_expr), |(_, rel_path)| { - Expr::Path(PathExpr { - is_absolute: true, - is_descendant: true, - steps: match rel_path { - Expr::Path(p) => p.steps, - _ => unreachable!(), - }, - }) - }), - // "/" RelativePathExpr? - map(pair(char('/'), opt(relative_path_expr)), |(_, rel_path)| { - Expr::Path(PathExpr { - is_absolute: true, - is_descendant: false, - steps: rel_path - .map(|p| match p { + map( + pair(tag("//"), move |i| relative_path_expr(true, i)), + |(_, rel_path)| { + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: true, + steps: match rel_path { Expr::Path(p) => p.steps, _ => unreachable!(), - }) - .unwrap_or_default(), - }) - }), + }, + }) + }, + ), + // "/" RelativePathExpr? + map( + pair(char('/'), opt(move |i| relative_path_expr(false, i))), + |(_, rel_path)| { + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: false, + steps: rel_path + .map(|p| match p { + Expr::Path(p) => p.steps, + _ => unreachable!(), + }) + .unwrap_or_default(), + }) + }, + ), // RelativePathExpr - relative_path_expr, + move |i| relative_path_expr(false, i), ))(input) } -fn relative_path_expr(input: &str) -> IResult<&str, Expr> { - let (input, first) = step_expr(input)?; +fn relative_path_expr(is_descendant: bool, input: &str) -> IResult<&str, Expr> { + let (input, first) = step_expr(is_descendant, input)?; let (input, steps) = many0(pair( - // ("/" | "//") - ws(alt((value(false, char('/')), value(true, tag("//"))))), - step_expr, + ws(alt((value(true, tag("//")), value(false, char('/'))))), + move |i| step_expr(is_descendant, i), ))(input)?; let mut all_steps = vec![first]; @@ -569,16 +574,18 @@ fn relative_path_expr(input: &str) -> IResult<&str, Expr> { )) } -fn step_expr(input: &str) -> IResult<&str, StepExpr> { +fn step_expr(is_descendant: bool, input: &str) -> IResult<&str, StepExpr> { alt(( map(filter_expr, StepExpr::Filter), - map(axis_step, StepExpr::Axis), + map(|i| axis_step(is_descendant, i), StepExpr::Axis), ))(input) } -fn axis_step(input: &str) -> IResult<&str, AxisStep> { - let (input, (step, predicates)) = - pair(alt((forward_step, reverse_step)), predicate_list)(input)?; +fn axis_step(is_descendant: bool, input: &str) -> IResult<&str, AxisStep> { + let (input, (step, predicates)) = pair( + alt((move |i| forward_step(is_descendant, i), reverse_step)), + predicate_list, + )(input)?; let (axis, node_test) = step; Ok(( @@ -591,13 +598,10 @@ fn axis_step(input: &str) -> IResult<&str, AxisStep> { )) } -fn forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { - alt(( - // ForwardAxis NodeTest - pair(forward_axis, node_test), - // AbbrevForwardStep - abbrev_forward_step, - ))(input) +fn forward_step(is_descendant: bool, input: &str) -> IResult<&str, (Axis, NodeTest)> { + alt((pair(forward_axis, node_test), move |i| { + abbrev_forward_step(is_descendant, i) + }))(input) } fn forward_axis(input: &str) -> IResult<&str, Axis> { @@ -615,7 +619,7 @@ fn forward_axis(input: &str) -> IResult<&str, Axis> { Ok((input, axis)) } -fn abbrev_forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { +fn abbrev_forward_step(is_descendant: bool, input: &str) -> IResult<&str, (Axis, NodeTest)> { let (input, attr) = opt(char('@'))(input)?; let (input, test) = node_test(input)?; @@ -624,6 +628,8 @@ fn abbrev_forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { ( if attr.is_some() { Axis::Attribute + } else if is_descendant { + Axis::DescendantOrSelf } else { Axis::Child }, @@ -704,6 +710,7 @@ fn filter_expr(input: &str) -> IResult<&str, FilterExpr> { fn predicate_list(input: &str) -> IResult<&str, PredicateListExpr> { let (input, predicates) = many0(predicate)(input)?; + Ok((input, PredicateListExpr { predicates })) } @@ -1195,6 +1202,118 @@ mod tests { ], }), ), + ( + "//mu[@xml:id=\"id1\"]//rho[@title][@xml:lang=\"en-GB\"]", + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: true, + steps: vec![ + StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Name(QName { + prefix: None, + local_part: "mu".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![PredicateExpr { + expr: Expr::Equality( + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Axis(AxisStep { + axis: Axis::Attribute, + node_test: NodeTest::Name(QName { + prefix: Some("xml".to_string()), + local_part: "id".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + EqualityOp::Eq, + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "id1".to_string(), + )), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + ), + }], + }, + }), + StepExpr::Axis(AxisStep { + axis: Axis::DescendantOrSelf, // Represents the second '//' + node_test: NodeTest::Kind(KindTest::Node), + predicates: PredicateListExpr { predicates: vec![] }, + }), + StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Name(QName { + prefix: None, + local_part: "rho".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![ + PredicateExpr { + expr: Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Axis(AxisStep { + axis: Axis::Attribute, + node_test: NodeTest::Name(QName { + prefix: None, + local_part: "title".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + }), + }, + PredicateExpr { + expr: Expr::Equality( + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Axis(AxisStep { + axis: Axis::Attribute, + node_test: NodeTest::Name(QName { + prefix: Some("xml".to_string()), + local_part: "lang".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + EqualityOp::Eq, + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "en-GB".to_string(), + )), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + ), + }, + ], + }, + }), + ], + }), + ), ]; for (input, expected) in cases { diff --git a/components/script_bindings/build.rs b/components/script_bindings/build.rs index b11e3d1cfdf..d2ff8ceb4a1 100644 --- a/components/script_bindings/build.rs +++ b/components/script_bindings/build.rs @@ -27,7 +27,7 @@ fn main() { println!("cargo::rerun-if-changed=../../third_party/WebIDL/WebIDL.py"); // NB: We aren't handling changes in `third_party/ply` here. - let status = Command::new(find_python()) + let status = find_python() .arg("codegen/run.py") .arg(&css_properties_json) .arg(&out_dir) @@ -78,55 +78,20 @@ impl phf_shared::PhfHash for Bytes<'_> { } } -/// Tries to find a suitable python +/// Tries to find a suitable python, which in Servo is always `uv run python` unless we are running +/// as a descendant of `uv run python`. In that case, we can use either `uv run python` or `python` +/// (uv does not provide a `python3` on Windows). /// -/// Algorithm -/// 1. Trying to find python3/python in $VIRTUAL_ENV (this should be from Servo's venv) -/// 2. Checking PYTHON3 (set by mach) -/// 3. Falling back to the system installation. +/// More details: <https://book.servo.org/hacking/setting-up-your-environment.html#check-tools> /// -/// Note: This function should be kept in sync with the version in `components/servo/build.rs` -fn find_python() -> PathBuf { - let mut candidates = vec![]; - if let Some(venv) = env::var_os("VIRTUAL_ENV") { - let bin_directory = PathBuf::from(venv).join("bin"); +/// Note: This function should be kept in sync with the version in `components/script/build.rs` +fn find_python() -> Command { + let mut command = Command::new("uv"); + command.args(["run", "python"]); - let python3 = bin_directory.join("python3"); - if python3.exists() { - candidates.push(python3); - } - let python = bin_directory.join("python"); - if python.exists() { - candidates.push(python); - } - }; - if let Some(python3) = env::var_os("PYTHON3") { - let python3 = PathBuf::from(python3); - if python3.exists() { - candidates.push(python3); - } + if command.output().is_ok_and(|out| out.status.success()) { + return command; } - let system_python = ["python3", "python"].map(PathBuf::from); - candidates.extend_from_slice(&system_python); - - for name in &candidates { - // Command::new() allows us to omit the `.exe` suffix on windows - if Command::new(name) - .arg("--version") - .output() - .is_ok_and(|out| out.status.success()) - { - return name.to_owned(); - } - } - let candidates = candidates - .into_iter() - .map(|c| c.into_os_string()) - .collect::<Vec<_>>(); - panic!( - "Can't find python (tried {:?})! Try enabling Servo's Python venv, \ - setting the PYTHON3 env var or adding python3 to PATH.", - candidates.join(", ".as_ref()) - ) + panic!("Can't find python (tried `{command:?}`)! Is uv installed and in PATH?") } diff --git a/components/script_bindings/codegen/Bindings.conf b/components/script_bindings/codegen/Bindings.conf index 7cc092e574e..edc099a823f 100644 --- a/components/script_bindings/codegen/Bindings.conf +++ b/components/script_bindings/codegen/Bindings.conf @@ -14,6 +14,10 @@ DOMInterfaces = { +'AbortController': { + 'canGc':['Abort'], +}, + 'AbstractRange': { 'weakReferenceable': True, }, @@ -642,7 +646,7 @@ DOMInterfaces = { }, 'Window': { - 'canGc': ['Stop', 'Fetch', 'Scroll', 'Scroll_','ScrollBy', 'ScrollBy_', 'Stop', 'Fetch', 'Open', 'CreateImageBitmap', 'TrustedTypes', 'WebdriverCallback', 'WebdriverException'], + 'canGc': ['Stop', 'Fetch', 'Scroll', 'Scroll_','ScrollBy', 'ScrollBy_', 'Stop', 'Fetch', 'Open', 'CreateImageBitmap', 'CreateImageBitmap_', 'TrustedTypes', 'WebdriverCallback', 'WebdriverException'], 'inRealms': ['Fetch', 'GetOpener', 'WebdriverCallback', 'WebdriverException'], 'additionalTraits': ['crate::interfaces::WindowHelpers'], }, @@ -654,7 +658,7 @@ DOMInterfaces = { 'WorkerGlobalScope': { 'inRealms': ['Fetch'], - 'canGc': ['Fetch', 'CreateImageBitmap', 'ImportScripts', 'TrustedTypes'], + 'canGc': ['Fetch', 'CreateImageBitmap', 'CreateImageBitmap_', 'ImportScripts', 'TrustedTypes'], }, 'Worklet': { diff --git a/components/script_bindings/webidls/AbortController.webidl b/components/script_bindings/webidls/AbortController.webidl index cef49010d3c..bb20ec24955 100644 --- a/components/script_bindings/webidls/AbortController.webidl +++ b/components/script_bindings/webidls/AbortController.webidl @@ -7,7 +7,7 @@ interface AbortController { constructor(); - //[SameObject] readonly attribute AbortSignal signal; + [SameObject] readonly attribute AbortSignal signal; undefined abort(optional any reason); }; diff --git a/components/script_bindings/webidls/CanvasRenderingContext2D.webidl b/components/script_bindings/webidls/CanvasRenderingContext2D.webidl index 47612a29937..b277ec2df41 100644 --- a/components/script_bindings/webidls/CanvasRenderingContext2D.webidl +++ b/components/script_bindings/webidls/CanvasRenderingContext2D.webidl @@ -11,7 +11,7 @@ typedef HTMLImageElement HTMLOrSVGImageElement; typedef (HTMLOrSVGImageElement or HTMLVideoElement or HTMLCanvasElement or - /*ImageBitmap or*/ + ImageBitmap or OffscreenCanvas or /*VideoFrame or*/ /*CSSImageValue*/ CSSStyleValue) CanvasImageSource; diff --git a/components/script_bindings/webidls/Element.webidl b/components/script_bindings/webidls/Element.webidl index 4545b18d058..e0073f856ca 100644 --- a/components/script_bindings/webidls/Element.webidl +++ b/components/script_bindings/webidls/Element.webidl @@ -144,3 +144,8 @@ Element includes NonDocumentTypeChildNode; Element includes ParentNode; Element includes ActivatableElement; Element includes ARIAMixin; + +// https://drafts.csswg.org/css-shadow-parts/#idl +partial interface Element { + [SameObject, PutForwards=value] readonly attribute DOMTokenList part; +}; diff --git a/components/script_bindings/webidls/HTMLHRElement.webidl b/components/script_bindings/webidls/HTMLHRElement.webidl index 8963d5e8901..45828d4da76 100644 --- a/components/script_bindings/webidls/HTMLHRElement.webidl +++ b/components/script_bindings/webidls/HTMLHRElement.webidl @@ -12,14 +12,9 @@ interface HTMLHRElement : HTMLElement { // https://html.spec.whatwg.org/multipage/#HTMLHRElement-partial partial interface HTMLHRElement { - [CEReactions] - attribute DOMString align; - [CEReactions] - attribute DOMString color; - // [CEReactions] - // attribute boolean noShade; - // [CEReactions] - // attribute DOMString size; - [CEReactions] - attribute DOMString width; + [CEReactions] attribute DOMString align; + [CEReactions] attribute DOMString color; + [CEReactions] attribute boolean noShade; + [CEReactions] attribute DOMString size; + [CEReactions] attribute DOMString width; }; diff --git a/components/script_bindings/webidls/HTMLLinkElement.webidl b/components/script_bindings/webidls/HTMLLinkElement.webidl index 9182ef393f8..db858a022c2 100644 --- a/components/script_bindings/webidls/HTMLLinkElement.webidl +++ b/components/script_bindings/webidls/HTMLLinkElement.webidl @@ -13,7 +13,7 @@ interface HTMLLinkElement : HTMLElement { attribute DOMString? crossOrigin; [CEReactions] attribute DOMString rel; - // [CEReactions] attribute DOMString as; + [CEReactions] attribute DOMString as; [SameObject, PutForwards=value] readonly attribute DOMTokenList relList; [CEReactions] attribute DOMString media; diff --git a/components/script_bindings/webidls/HTMLScriptElement.webidl b/components/script_bindings/webidls/HTMLScriptElement.webidl index 2c7b398b7e3..a21ae6007c4 100644 --- a/components/script_bindings/webidls/HTMLScriptElement.webidl +++ b/components/script_bindings/webidls/HTMLScriptElement.webidl @@ -32,6 +32,8 @@ interface HTMLScriptElement : HTMLElement { [CEReactions] attribute DOMString referrerPolicy; + static boolean supports(DOMString type); + // also has obsolete members }; diff --git a/components/script_bindings/webidls/WebGLRenderingContext.webidl b/components/script_bindings/webidls/WebGLRenderingContext.webidl index 6938e547cce..645eef0c23f 100644 --- a/components/script_bindings/webidls/WebGLRenderingContext.webidl +++ b/components/script_bindings/webidls/WebGLRenderingContext.webidl @@ -24,7 +24,8 @@ typedef unsigned long GLuint; typedef unrestricted float GLfloat; typedef unrestricted float GLclampf; -typedef (ImageData or +typedef (ImageBitmap or + ImageData or HTMLImageElement or HTMLCanvasElement or HTMLVideoElement) TexImageSource; diff --git a/components/script_bindings/webidls/WindowOrWorkerGlobalScope.webidl b/components/script_bindings/webidls/WindowOrWorkerGlobalScope.webidl index deb3d5e2947..e329048b1fb 100644 --- a/components/script_bindings/webidls/WindowOrWorkerGlobalScope.webidl +++ b/components/script_bindings/webidls/WindowOrWorkerGlobalScope.webidl @@ -26,8 +26,9 @@ interface mixin WindowOrWorkerGlobalScope { // ImageBitmap [Pref="dom_imagebitmap_enabled"] Promise<ImageBitmap> createImageBitmap(ImageBitmapSource image, optional ImageBitmapOptions options = {}); - // Promise<ImageBitmap> createImageBitmap( - // ImageBitmapSource image, long sx, long sy, long sw, long sh, optional ImageBitmapOptions options); + [Pref="dom_imagebitmap_enabled"] + Promise<ImageBitmap> createImageBitmap(ImageBitmapSource image, long sx, long sy, long sw, long sh, + optional ImageBitmapOptions options = {}); // structured cloning [Throws] diff --git a/components/servo/build.rs b/components/servo/build.rs index 0b6b087547b..d610d1cfe36 100644 --- a/components/servo/build.rs +++ b/components/servo/build.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use std::{env, fs}; @@ -10,7 +10,7 @@ fn main() { if cfg!(feature = "media-gstreamer") { println!("cargo:rerun-if-changed=../../python/servo/gstreamer.py"); - let output = Command::new(find_python()) + let output = find_python() .arg("../../python/servo/gstreamer.py") .arg(std::env::var_os("TARGET").unwrap()) .output() @@ -25,47 +25,20 @@ fn main() { } } -/// Tries to find a suitable python +/// Tries to find a suitable python, which in Servo is always `uv run python` unless we are running +/// as a descendant of `uv run python`. In that case, we can use either `uv run python` or `python` +/// (uv does not provide a `python3` on Windows). /// -/// Algorithm -/// 1. Trying to find python3/python in $VIRTUAL_ENV (this should be from servos venv) -/// 2. Checking PYTHON3 (set by mach) -/// 3. Falling back to the system installation. +/// More details: <https://book.servo.org/hacking/setting-up-your-environment.html#check-tools> /// /// Note: This function should be kept in sync with the version in `components/script/build.rs` -fn find_python() -> PathBuf { - let mut candidates = vec![]; - if let Some(venv) = env::var_os("VIRTUAL_ENV") { - // See: https://docs.python.org/3/library/venv.html#how-venvs-work - let bin_dir = if cfg!(windows) { "Scripts" } else { "bin" }; - let bin_directory = PathBuf::from(venv).join(bin_dir); - candidates.push(bin_directory.join("python3")); - candidates.push(bin_directory.join("python")); - } - if let Some(python3) = env::var_os("PYTHON3") { - candidates.push(PathBuf::from(python3)); - } - - let system_python = ["python3", "python"].map(PathBuf::from); - candidates.extend_from_slice(&system_python); +fn find_python() -> Command { + let mut command = Command::new("uv"); + command.args(["run", "python"]); - for name in &candidates { - // Command::new() allows us to omit the `.exe` suffix on windows - if Command::new(name) - .arg("--version") - .output() - .is_ok_and(|out| out.status.success()) - { - return name.to_owned(); - } + if command.output().is_ok_and(|out| out.status.success()) { + return command; } - let candidates = candidates - .into_iter() - .map(|c| c.into_os_string()) - .collect::<Vec<_>>(); - panic!( - "Can't find python (tried {:?})! Try enabling Servo's Python venv, \ - setting the PYTHON3 env var or adding python3 to PATH.", - candidates.join(", ".as_ref()) - ) + + panic!("Can't find python (tried `{command:?}`)! Is uv installed and in PATH?") } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index f2732eaddfe..84967f22107 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -92,6 +92,7 @@ use media::{GlApi, NativeDisplay, WindowGLContext}; use net::protocols::ProtocolRegistry; use net::resource_thread::new_resource_threads; use profile::{mem as profile_mem, time as profile_time}; +use profile_traits::mem::MemoryReportResult; use profile_traits::{mem, time}; use script::{JSEngineSetup, ServiceWorkerManager}; use servo_config::opts::Opts; @@ -417,7 +418,7 @@ impl Servo { // Create the WebXR main thread #[cfg(feature = "webxr")] let mut webxr_main_thread = - webxr::MainThreadRegistry::new(event_loop_waker, webxr_layer_grand_manager) + webxr::MainThreadRegistry::new(event_loop_waker.clone(), webxr_layer_grand_manager) .expect("Failed to create WebXR device registry"); #[cfg(feature = "webxr")] if pref!(dom_webxr_enabled) { @@ -489,6 +490,7 @@ impl Servo { #[cfg(feature = "webxr")] webxr_main_thread, shutdown_state: shutdown_state.clone(), + event_loop_waker, }, opts.debug.convert_mouse_to_touch, ); @@ -546,7 +548,14 @@ impl Servo { return false; } - self.compositor.borrow_mut().receive_messages(); + { + let mut compositor = self.compositor.borrow_mut(); + let mut messages = Vec::new(); + while let Ok(message) = compositor.receiver().try_recv() { + messages.push(message); + } + compositor.handle_messages(messages); + } // Only handle incoming embedder messages if the compositor hasn't already started shutting down. while let Ok(message) = self.embedder_receiver.try_recv() { @@ -632,6 +641,11 @@ impl Servo { log::set_max_level(filter); } + pub fn create_memory_report(&self, snd: IpcSender<MemoryReportResult>) { + self.constellation_proxy + .send(EmbedderToConstellationMessage::CreateMemoryReport(snd)); + } + pub fn start_shutting_down(&self) { if self.shutdown_state.get() != ShutdownState::NotShuttingDown { warn!("Requested shutdown while already shutting down"); diff --git a/components/shared/canvas/canvas.rs b/components/shared/canvas/canvas.rs index 850f5f9bd9a..283be0d458e 100644 --- a/components/shared/canvas/canvas.rs +++ b/components/shared/canvas/canvas.rs @@ -89,8 +89,8 @@ pub enum Canvas2dMsg { Arc(Point2D<f32>, f32, f32, f32, bool), ArcTo(Point2D<f32>, Point2D<f32>, f32), DrawImage(IpcSnapshot, Rect<f64>, Rect<f64>, bool), - DrawEmptyImage(Size2D<f64>, Rect<f64>, Rect<f64>), - DrawImageInOther(CanvasId, Size2D<f64>, Rect<f64>, Rect<f64>, bool), + DrawEmptyImage(Size2D<u32>, Rect<f64>, Rect<f64>), + DrawImageInOther(CanvasId, Size2D<u32>, Rect<f64>, Rect<f64>, bool), BeginPath, BezierCurveTo(Point2D<f32>, Point2D<f32>, Point2D<f32>), ClearRect(Rect<f32>), @@ -102,14 +102,14 @@ pub enum Canvas2dMsg { FillPath(FillOrStrokeStyle, Vec<PathSegment>), FillText(String, f64, f64, Option<f64>, FillOrStrokeStyle, bool), FillRect(Rect<f32>, FillOrStrokeStyle), - GetImageData(Rect<u64>, Size2D<u64>, IpcSender<IpcSnapshot>), + GetImageData(Rect<u32>, Size2D<u32>, IpcSender<IpcSnapshot>), GetTransform(IpcSender<Transform2D<f32>>), IsPointInCurrentPath(f64, f64, FillRule, IpcSender<bool>), IsPointInPath(Vec<PathSegment>, f64, f64, FillRule, IpcSender<bool>), LineTo(Point2D<f32>), MoveTo(Point2D<f32>), MeasureText(String, IpcSender<TextMetrics>), - PutImageData(Rect<u64>, IpcBytesReceiver), + PutImageData(Rect<u32>, IpcBytesReceiver), QuadraticCurveTo(Point2D<f32>, Point2D<f32>), Rect(Rect<f32>), RestoreContext, diff --git a/components/shared/canvas/lib.rs b/components/shared/canvas/lib.rs index 826db54b0f6..df6b0854b9e 100644 --- a/components/shared/canvas/lib.rs +++ b/components/shared/canvas/lib.rs @@ -21,5 +21,5 @@ pub enum ConstellationCanvasMsg { sender: Sender<(CanvasId, ImageKey)>, size: Size2D<u64>, }, - Exit, + Exit(Sender<()>), } diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index 67ff0046885..c2acb83f240 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -23,6 +23,7 @@ use webrender_api::DocumentId; pub mod display_list; pub mod rendering_context; +pub mod viewport_description; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -42,6 +43,8 @@ use webrender_api::{ ImageKey, NativeFontHandle, PipelineId as WebRenderPipelineId, }; +use crate::viewport_description::ViewportDescription; + /// Sends messages to the compositor. #[derive(Clone)] pub struct CompositorProxy { @@ -108,12 +111,12 @@ pub enum CompositorMsg { MouseButton, f32, f32, - WebDriverMessageId, + Option<WebDriverMessageId>, ), /// WebDriver mouse move event - WebDriverMouseMoveEvent(WebViewId, f32, f32, WebDriverMessageId), + WebDriverMouseMoveEvent(WebViewId, f32, f32, Option<WebDriverMessageId>), // Webdriver wheel scroll event - WebDriverWheelScrollEvent(WebViewId, f32, f32, f64, f64), + WebDriverWheelScrollEvent(WebViewId, f32, f32, f64, f64, Option<WebDriverMessageId>), /// Inform WebRender of the existence of this pipeline. SendInitialTransaction(WebRenderPipelineId), @@ -176,6 +179,8 @@ pub enum CompositorMsg { /// Measure the current memory usage associated with the compositor. /// The report must be sent on the provided channel once it's complete. CollectMemoryReport(ReportsChan), + /// A top-level frame has parsed a viewport metatag and is sending the new constraints. + Viewport(WebViewId, ViewportDescription), } impl Debug for CompositorMsg { diff --git a/components/shared/compositing/viewport_description.rs b/components/shared/compositing/viewport_description.rs new file mode 100644 index 00000000000..83b29371f84 --- /dev/null +++ b/components/shared/compositing/viewport_description.rs @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This module contains helpers for Viewport + +use std::collections::HashMap; +use std::str::FromStr; + +use euclid::default::Scale; +use serde::{Deserialize, Serialize}; + +/// Default viewport constraints +/// +/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale> +pub const MIN_ZOOM: f32 = 0.1; +/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale> +pub const MAX_ZOOM: f32 = 10.0; +/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale> +pub const DEFAULT_ZOOM: f32 = 1.0; + +/// <https://drafts.csswg.org/css-viewport/#parsing-algorithm> +const SEPARATORS: [char; 2] = [',', ';']; // Comma (0x2c) and Semicolon (0x3b) + +/// A set of viewport descriptors: +/// +/// <https://www.w3.org/TR/css-viewport-1/#viewport-meta> +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ViewportDescription { + // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#width + // the (minimum width) size of the viewport + // TODO: width Needs to be implemented + // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#width + // the (minimum height) size of the viewport + // TODO: height Needs to be implemented + /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale> + /// the zoom level when the page is first loaded + pub initial_scale: Scale<f32>, + + /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#minimum_scale> + /// how much zoom out is allowed on the page. + pub minimum_scale: Scale<f32>, + + /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#maximum_scale> + /// how much zoom in is allowed on the page + pub maximum_scale: Scale<f32>, + + /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#user_scalable> + /// whether zoom in and zoom out actions are allowed on the page + pub user_scalable: UserScalable, +} + +/// The errors that the viewport parsing can generate. +#[derive(Debug)] +pub enum ViewportDescriptionParseError { + /// When viewport attribute string is empty + Empty, +} + +/// A set of User Zoom values: +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum UserScalable { + /// Zoom is not allowed + No = 0, + /// Zoom is allowed + Yes = 1, +} + +/// Parses a viewport user scalable value. +impl TryFrom<&str> for UserScalable { + type Error = &'static str; + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value.to_lowercase().as_str() { + "yes" => Ok(UserScalable::Yes), + "no" => Ok(UserScalable::No), + _ => match value.parse::<f32>() { + Ok(1.0) => Ok(UserScalable::Yes), + Ok(0.0) => Ok(UserScalable::No), + _ => Err("can't convert character to UserScalable"), + }, + } + } +} + +impl Default for ViewportDescription { + fn default() -> Self { + ViewportDescription { + initial_scale: Scale::new(DEFAULT_ZOOM), + minimum_scale: Scale::new(MIN_ZOOM), + maximum_scale: Scale::new(MAX_ZOOM), + user_scalable: UserScalable::Yes, + } + } +} + +impl ViewportDescription { + /// Iterates over the key-value pairs generated from meta tag and returns a ViewportDescription + fn process_viewport_key_value_pairs(pairs: HashMap<String, String>) -> ViewportDescription { + let mut description = ViewportDescription::default(); + for (key, value) in &pairs { + match key.as_str() { + "initial-scale" => { + if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) { + description.initial_scale = zoom; + } + }, + "minimum-scale" => { + if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) { + description.minimum_scale = zoom; + } + }, + "maximum-scale" => { + if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) { + description.maximum_scale = zoom; + } + }, + "user-scalable" => { + if let Ok(user_zoom_allowed) = value.as_str().try_into() { + description.user_scalable = user_zoom_allowed; + } + }, + _ => (), + } + } + description + } + + /// Parses a viewport zoom value. + fn parse_viewport_value_as_zoom(value: &str) -> Option<Scale<f32>> { + value + .to_lowercase() + .as_str() + .parse::<f32>() + .ok() + .filter(|&n| (0.0..=10.0).contains(&n)) + .map(Scale::new) + } + + /// Constrains a zoom value within the allowed scale range + pub fn clamp_zoom(&self, zoom: f32) -> f32 { + zoom.clamp(self.minimum_scale.get(), self.maximum_scale.get()) + } +} + +/// <https://drafts.csswg.org/css-viewport/#parsing-algorithm> +/// +/// This implementation differs from the specified algorithm, but is equivalent because +/// 1. It uses higher-level string operations to process string instead of character-by-character iteration. +/// 2. Uses trim() operation to handle whitespace instead of explicitly handling throughout the parsing process. +impl FromStr for ViewportDescription { + type Err = ViewportDescriptionParseError; + fn from_str(string: &str) -> Result<Self, Self::Err> { + if string.is_empty() { + return Err(ViewportDescriptionParseError::Empty); + } + + // Parse key-value pairs from the content string + // 1. Split the content string using SEPARATORS + // 2. Split into key-value pair using "=" and trim whitespaces + // 3. Insert into HashMap + let parsed_values = string + .split(SEPARATORS) + .filter_map(|pair| { + let mut parts = pair.split('=').map(str::trim); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + Some((key.to_string(), value.to_string())) + } else { + None + } + }) + .collect::<HashMap<String, String>>(); + + Ok(Self::process_viewport_key_value_pairs(parsed_values)) + } +} diff --git a/components/shared/constellation/lib.rs b/components/shared/constellation/lib.rs index fb00d4afdbf..53e924c5ad5 100644 --- a/components/shared/constellation/lib.rs +++ b/components/shared/constellation/lib.rs @@ -26,6 +26,7 @@ use euclid::Vector2D; pub use from_script_message::*; use ipc_channel::ipc::IpcSender; use malloc_size_of_derive::MallocSizeOf; +use profile_traits::mem::MemoryReportResult; use serde::{Deserialize, Serialize}; use servo_url::{ImmutableOrigin, ServoUrl}; pub use structured_data::*; @@ -95,6 +96,8 @@ pub enum EmbedderToConstellationMessage { /// Evaluate a JavaScript string in the context of a `WebView`. When execution is complete or an /// error is encountered, a correpsonding message will be sent to the embedding layer. EvaluateJavaScript(WebViewId, JavaScriptEvaluationId, String), + /// Create a memory report and return it via the ipc sender + CreateMemoryReport(IpcSender<MemoryReportResult>), } /// A description of a paint metric that is sent from the Servo renderer to the diff --git a/components/shared/embedder/input_events.rs b/components/shared/embedder/input_events.rs index 869c4eee004..d5c0c7bc46a 100644 --- a/components/shared/embedder/input_events.rs +++ b/components/shared/embedder/input_events.rs @@ -54,24 +54,26 @@ impl InputEvent { InputEvent::MouseButton(event) => event.webdriver_id, InputEvent::MouseMove(event) => event.webdriver_id, InputEvent::Touch(..) => None, - InputEvent::Wheel(..) => None, + InputEvent::Wheel(event) => event.webdriver_id, } } - pub fn with_webdriver_message_id(self, webdriver_id: Option<WebDriverMessageId>) -> Self { + pub fn with_webdriver_message_id(mut self, webdriver_id: Option<WebDriverMessageId>) -> Self { match self { InputEvent::EditingAction(..) => {}, InputEvent::Gamepad(..) => {}, InputEvent::Ime(..) => {}, InputEvent::Keyboard(..) => {}, - InputEvent::MouseButton(mut event) => { + InputEvent::MouseButton(ref mut event) => { event.webdriver_id = webdriver_id; }, - InputEvent::MouseMove(mut event) => { + InputEvent::MouseMove(ref mut event) => { event.webdriver_id = webdriver_id; }, InputEvent::Touch(..) => {}, - InputEvent::Wheel(..) => {}, + InputEvent::Wheel(ref mut event) => { + event.webdriver_id = webdriver_id; + }, }; self @@ -275,6 +277,17 @@ pub struct WheelDelta { pub struct WheelEvent { pub delta: WheelDelta, pub point: DevicePoint, + webdriver_id: Option<WebDriverMessageId>, +} + +impl WheelEvent { + pub fn new(delta: WheelDelta, point: DevicePoint) -> Self { + WheelEvent { + delta, + point, + webdriver_id: None, + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index 7b0f02bc26a..4333fa9fe2b 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -50,7 +50,8 @@ pub enum WebDriverCommandMsg { MouseButton, f32, f32, - WebDriverMessageId, + // Should never be None. + Option<WebDriverMessageId>, IpcSender<WebDriverCommandResponse>, ), /// Act as if the mouse was moved in the browsing context with the given ID. @@ -58,11 +59,23 @@ pub enum WebDriverCommandMsg { WebViewId, f32, f32, - WebDriverMessageId, + // None if it's not the last `perform_pointer_move` since we only + // expect one response from constellation for each tick actions. + Option<WebDriverMessageId>, IpcSender<WebDriverCommandResponse>, ), /// Act as if the mouse wheel is scrolled in the browsing context given the given ID. - WheelScrollAction(WebViewId, f32, f32, f64, f64), + WheelScrollAction( + WebViewId, + f32, + f32, + f64, + f64, + // None if it's not the last `perform_wheel_scroll` since we only + // expect one response from constellation for each tick actions. + Option<WebDriverMessageId>, + IpcSender<WebDriverCommandResponse>, + ), /// Set the window size. SetWindowSize(WebViewId, DeviceIntSize, IpcSender<Size2D<f32, CSSPixel>>), /// Take a screenshot of the window. @@ -84,6 +97,10 @@ pub enum WebDriverCommandMsg { CloseWebView(WebViewId), /// Focus the webview associated with the provided id. FocusWebView(WebViewId), + /// Check whether top-level browsing context is open. + IsWebViewOpen(WebViewId, IpcSender<bool>), + /// Check whether browsing context is open. + IsBrowsingContextOpen(BrowsingContextId, IpcSender<bool>), } #[derive(Debug, Deserialize, Serialize)] @@ -94,7 +111,7 @@ pub enum WebDriverScriptCommand { serialize_with = "::hyper_serde::serialize" )] Cookie<'static>, - IpcSender<Result<(), WebDriverCookieError>>, + IpcSender<Result<(), ErrorStatus>>, ), DeleteCookies(IpcSender<Result<(), ErrorStatus>>), DeleteCookie(String, IpcSender<Result<(), ErrorStatus>>), @@ -130,12 +147,15 @@ pub enum WebDriverScriptCommand { IpcSender<Result<Vec<String>, ErrorStatus>>, ), FindElementElementsTagName(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>), - FocusElement(String, IpcSender<Result<(), ErrorStatus>>), + GetElementShadowRoot(String, IpcSender<Result<Option<String>, ErrorStatus>>), ElementClick(String, IpcSender<Result<Option<String>, ErrorStatus>>), GetActiveElement(IpcSender<Option<String>>), GetComputedRole(String, IpcSender<Result<Option<String>, ErrorStatus>>), - GetCookie(String, IpcSender<Vec<Serde<Cookie<'static>>>>), - GetCookies(IpcSender<Vec<Serde<Cookie<'static>>>>), + GetCookie( + String, + IpcSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>, + ), + GetCookies(IpcSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>), GetElementAttribute( String, String, @@ -161,12 +181,8 @@ pub enum WebDriverScriptCommand { IsEnabled(String, IpcSender<Result<bool, ErrorStatus>>), IsSelected(String, IpcSender<Result<bool, ErrorStatus>>), GetTitle(IpcSender<String>), -} - -#[derive(Debug, Deserialize, Serialize)] -pub enum WebDriverCookieError { - InvalidDomain, - UnableToSetCookie, + /// Match the element type before sending the event for webdriver `element send keys`. + WillSendKeys(String, String, bool, IpcSender<Result<bool, ErrorStatus>>), } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/components/shared/net/lib.rs b/components/shared/net/lib.rs index 0126bdbcd80..0a490fa3bcf 100644 --- a/components/shared/net/lib.rs +++ b/components/shared/net/lib.rs @@ -40,6 +40,7 @@ pub mod blob_url_store; pub mod filemanager_thread; pub mod http_status; pub mod image_cache; +pub mod mime_classifier; pub mod policy_container; pub mod pub_domains; pub mod quality; diff --git a/components/net/mime_classifier.rs b/components/shared/net/mime_classifier.rs index a98c4428b87..7e5408ba56e 100644 --- a/components/net/mime_classifier.rs +++ b/components/shared/net/mime_classifier.rs @@ -3,7 +3,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use mime::{self, Mime}; -use net_traits::LoadContext; + +use crate::LoadContext; pub struct MimeClassifier { image_classifier: GroupedClassifier, @@ -16,11 +17,15 @@ pub struct MimeClassifier { font_classifier: GroupedClassifier, } +#[derive(PartialEq)] pub enum MediaType { Xml, Html, AudioVideo, Image, + JavaScript, + Json, + Font, } pub enum ApacheBugFlag { @@ -99,7 +104,11 @@ impl MimeClassifier { Some(MediaType::AudioVideo) => { self.audio_video_classifier.classify(data) }, - Some(MediaType::Xml) | None => None, + Some(MediaType::JavaScript) | + Some(MediaType::Font) | + Some(MediaType::Json) | + Some(MediaType::Xml) | + None => None, } .unwrap_or(supplied_type.clone()) }, @@ -215,20 +224,24 @@ impl MimeClassifier { .expect("BinaryOrPlaintextClassifier always succeeds") } + /// <https://mimesniff.spec.whatwg.org/#xml-mime-type> fn is_xml(mt: &Mime) -> bool { mt.suffix() == Some(mime::XML) || - (mt.type_() == mime::APPLICATION && mt.subtype() == mime::XML) || - (mt.type_() == mime::TEXT && mt.subtype() == mime::XML) + *mt == mime::TEXT_XML || + (mt.type_() == mime::APPLICATION && mt.subtype() == mime::XML) } + /// <https://mimesniff.spec.whatwg.org/#html-mime-type> fn is_html(mt: &Mime) -> bool { - mt.type_() == mime::TEXT && mt.subtype() == mime::HTML + *mt == mime::TEXT_HTML } + /// <https://mimesniff.spec.whatwg.org/#image-mime-type> fn is_image(mt: &Mime) -> bool { mt.type_() == mime::IMAGE } + /// <https://mimesniff.spec.whatwg.org/#audio-or-video-mime-type> fn is_audio_video(mt: &Mime) -> bool { mt.type_() == mime::AUDIO || mt.type_() == mime::VIDEO || @@ -241,7 +254,53 @@ impl MimeClassifier { mt.type_() == mime::STAR && mt.subtype() == mime::STAR } - fn get_media_type(mime: &Mime) -> Option<MediaType> { + /// <https://mimesniff.spec.whatwg.org/#javascript-mime-type> + fn is_javascript(mt: &Mime) -> bool { + (mt.type_() == mime::APPLICATION && + (["ecmascript", "javascript", "x-ecmascript", "x-javascript"] + .contains(&mt.subtype().as_str()))) || + (mt.type_() == mime::TEXT && + ([ + "ecmascript", + "javascript", + "javascript1.0", + "javascript1.1", + "javascript1.2", + "javascript1.3", + "javascript1.4", + "javascript1.5", + "jscript", + "livescript", + "x-ecmascript", + "x-javascript", + ] + .contains(&mt.subtype().as_str()))) + } + + /// <https://mimesniff.spec.whatwg.org/#json-mime-type> + fn is_json(mt: &Mime) -> bool { + mt.suffix() == Some(mime::JSON) || + (mt.subtype() == mime::JSON && + (mt.type_() == mime::APPLICATION || mt.type_() == mime::TEXT)) + } + + /// <https://mimesniff.spec.whatwg.org/#font-mime-type> + fn is_font(mt: &Mime) -> bool { + mt.type_() == mime::FONT || + (mt.type_() == mime::APPLICATION && + ([ + "font-cff", + "font-off", + "font-sfnt", + "font-ttf", + "font-woff", + "vnd.ms-fontobject", + "vnd.ms-opentype", + ] + .contains(&mt.subtype().as_str()))) + } + + pub fn get_media_type(mime: &Mime) -> Option<MediaType> { if MimeClassifier::is_xml(mime) { Some(MediaType::Xml) } else if MimeClassifier::is_html(mime) { @@ -250,6 +309,12 @@ impl MimeClassifier { Some(MediaType::Image) } else if MimeClassifier::is_audio_video(mime) { Some(MediaType::AudioVideo) + } else if MimeClassifier::is_javascript(mime) { + Some(MediaType::JavaScript) + } else if MimeClassifier::is_font(mime) { + Some(MediaType::Font) + } else if MimeClassifier::is_json(mime) { + Some(MediaType::Json) } else { None } diff --git a/components/shared/net/request.rs b/components/shared/net/request.rs index 259132b55c4..7b39ac7b78d 100644 --- a/components/shared/net/request.rs +++ b/components/shared/net/request.rs @@ -9,6 +9,7 @@ use content_security_policy::{self as csp}; use http::header::{AUTHORIZATION, HeaderName}; use http::{HeaderMap, Method}; use ipc_channel::ipc::{self, IpcReceiver, IpcSender, IpcSharedMemory}; +use ipc_channel::router::ROUTER; use malloc_size_of_derive::MallocSizeOf; use mime::Mime; use serde::{Deserialize, Serialize}; @@ -925,3 +926,22 @@ pub fn convert_header_names_to_sorted_lowercase_set( ordered_set.dedup(); ordered_set.into_iter().cloned().collect() } + +pub fn create_request_body_with_content(content: &str) -> RequestBody { + let content_bytes = IpcSharedMemory::from_bytes(content.as_bytes()); + let content_len = content_bytes.len(); + + let (chunk_request_sender, chunk_request_receiver) = ipc::channel().unwrap(); + ROUTER.add_typed_route( + chunk_request_receiver, + Box::new(move |message| { + let request = message.unwrap(); + if let BodyChunkRequest::Connect(sender) = request { + let _ = sender.send(BodyChunkResponse::Chunk(content_bytes.clone())); + let _ = sender.send(BodyChunkResponse::Done); + } + }), + ); + + RequestBody::new(chunk_request_sender, BodySource::Object, Some(content_len)) +} diff --git a/components/net/tests/mime_classifier.rs b/components/shared/net/tests/mime_classifier.rs index b4e6ae6e9ab..79a122ac8bf 100644 --- a/components/net/tests/mime_classifier.rs +++ b/components/shared/net/tests/mime_classifier.rs @@ -8,8 +8,8 @@ use std::io::{self, Read}; use std::path::{self, PathBuf}; use mime::{self, Mime}; -use net::mime_classifier::{ApacheBugFlag, MimeClassifier, Mp4Matcher, NoSniffFlag}; use net_traits::LoadContext; +use net_traits::mime_classifier::{ApacheBugFlag, MimeClassifier, Mp4Matcher, NoSniffFlag}; fn read_file(path: &path::Path) -> io::Result<Vec<u8>> { let mut file = File::open(path)?; diff --git a/components/net/tests/parsable_mime/application/font-woff/test.wof b/components/shared/net/tests/parsable_mime/application/font-woff/test.wof index a1393ebee1d..a1393ebee1d 100755 --- a/components/net/tests/parsable_mime/application/font-woff/test.wof +++ b/components/shared/net/tests/parsable_mime/application/font-woff/test.wof diff --git a/components/net/tests/parsable_mime/application/ogg/small.ogg b/components/shared/net/tests/parsable_mime/application/ogg/small.ogg Binary files differindex 0d7f43eb795..0d7f43eb795 100644 --- a/components/net/tests/parsable_mime/application/ogg/small.ogg +++ b/components/shared/net/tests/parsable_mime/application/ogg/small.ogg diff --git a/components/net/tests/parsable_mime/application/pdf/test.pdf b/components/shared/net/tests/parsable_mime/application/pdf/test.pdf index e7c6e62775f..055169fe34b 100644 --- a/components/net/tests/parsable_mime/application/pdf/test.pdf +++ b/components/shared/net/tests/parsable_mime/application/pdf/test.pdf @@ -1,157 +1,157 @@ -%PDF-1.2
-%
-
-9 0 obj
-<<
-/Length 10 0 R
-/Filter /FlateDecode
->>
-stream
+%PDF-1.2 +% + +9 0 obj +<< +/Length 10 0 R +/Filter /FlateDecode +>> +stream H͐J0 {f$Mn-[&jeۤ~$}ɅIjs~X-],$Y)'Nu1!V?? -b1Rbb҉H[TD:#&حXi$qnf]a{أq|JLs]QIj%9`Uitez$OeBĒүR@zܗg<
-endstream
-endobj
-10 0 obj
-246
-endobj
-4 0 obj
-<<
-/Type /Page
-/Parent 5 0 R
-/Resources <<
-/Font <<
-/F0 6 0 R
-/F1 7 0 R
->>
-/ProcSet 2 0 R
->>
-/Contents 9 0 R
->>
-endobj
-6 0 obj
-<<
-/Type /Font
-/Subtype /TrueType
-/Name /F0
-/BaseFont /Arial
-/Encoding /WinAnsiEncoding
->>
-endobj
-7 0 obj
-<<
-/Type /Font
-/Subtype /TrueType
-/Name /F1
-/BaseFont /BookAntiqua,Bold
-/FirstChar 31
-/LastChar 255
-/Widths [ 750 250 278 402 606 500 889 833 227 333 333 444 606 250 333 250
-296 500 500 500 500 500 500 500 500 500 500 250 250 606 606 606
-444 747 778 667 722 833 611 556 833 833 389 389 778 611 1000 833
-833 611 833 722 611 667 778 778 1000 667 667 667 333 606 333 606
-500 333 500 611 444 611 500 389 556 611 333 333 611 333 889 611
-556 611 611 389 444 333 611 556 833 500 556 500 310 606 310 606
-750 500 750 333 500 500 1000 500 500 333 1000 611 389 1000 750 750
-750 750 278 278 500 500 606 500 1000 333 998 444 389 833 750 750
-667 250 278 500 500 606 500 606 500 333 747 438 500 606 333 747
-500 400 549 361 361 333 576 641 250 333 361 488 500 889 890 889
-444 778 778 778 778 778 778 1000 722 611 611 611 611 389 389 389
-389 833 833 833 833 833 833 833 606 833 778 778 778 778 667 611
-611 500 500 500 500 500 500 778 444 500 500 500 500 333 333 333
-333 556 611 556 556 556 556 556 549 556 611 611 611 611 556 611
-556 ]
-/Encoding /WinAnsiEncoding
-/FontDescriptor 8 0 R
->>
-endobj
-8 0 obj
-<<
-/Type /FontDescriptor
-/FontName /BookAntiqua,Bold
-/Flags 16418
-/FontBBox [ -250 -260 1236 930 ]
-/MissingWidth 750
-/StemV 146
-/StemH 146
-/ItalicAngle 0
-/CapHeight 930
-/XHeight 651
-/Ascent 930
-/Descent 260
-/Leading 210
-/MaxWidth 1030
-/AvgWidth 460
->>
-endobj
-2 0 obj
-[ /PDF /Text ]
-endobj
-5 0 obj
-<<
-/Kids [4 0 R ]
-/Count 1
-/Type /Pages
-/MediaBox [ 0 0 612 792 ]
->>
-endobj
-1 0 obj
-<<
-/Creator (1725.fm)
-/CreationDate (1-Jan-3 18:15PM)
-/Title (1725.PDF)
-/Author (Unknown)
-/Producer (Acrobat PDFWriter 3.02 for Windows)
-/Keywords ()
-/Subject ()
->>
-endobj
-3 0 obj
-<<
-/Pages 5 0 R
-/Type /Catalog
-/DefaultGray 11 0 R
-/DefaultRGB 12 0 R
->>
-endobj
-11 0 obj
-[/CalGray
-<<
-/WhitePoint [0.9505 1 1.0891 ]
-/Gamma 0.2468
->>
-]
-endobj
-12 0 obj
-[/CalRGB
-<<
-/WhitePoint [0.9505 1 1.0891 ]
-/Gamma [0.2468 0.2468 0.2468 ]
-/Matrix [0.4361 0.2225 0.0139 0.3851 0.7169 0.0971 0.1431 0.0606 0.7141 ]
->>
-]
-endobj
-xref
-0 13
-0000000000 65535 f
-0000002172 00000 n
-0000002046 00000 n
-0000002363 00000 n
-0000000375 00000 n
-0000002080 00000 n
-0000000518 00000 n
-0000000633 00000 n
-0000001760 00000 n
-0000000021 00000 n
-0000000352 00000 n
-0000002460 00000 n
-0000002548 00000 n
-trailer
-<<
-/Size 13
-/Root 3 0 R
-/Info 1 0 R
-/ID [<47149510433dd4882f05f8c124223734><47149510433dd4882f05f8c124223734>]
->>
-startxref
-2726
-%%EOF
+b1Rbb҉H[TD:#&حXi$qnf]a{أq|JLs]QIj%9`Uitez$OeBĒүR@zܗg< +endstream +endobj +10 0 obj +246 +endobj +4 0 obj +<< +/Type /Page +/Parent 5 0 R +/Resources << +/Font << +/F0 6 0 R +/F1 7 0 R +>> +/ProcSet 2 0 R +>> +/Contents 9 0 R +>> +endobj +6 0 obj +<< +/Type /Font +/Subtype /TrueType +/Name /F0 +/BaseFont /Arial +/Encoding /WinAnsiEncoding +>> +endobj +7 0 obj +<< +/Type /Font +/Subtype /TrueType +/Name /F1 +/BaseFont /BookAntiqua,Bold +/FirstChar 31 +/LastChar 255 +/Widths [ 750 250 278 402 606 500 889 833 227 333 333 444 606 250 333 250 +296 500 500 500 500 500 500 500 500 500 500 250 250 606 606 606 +444 747 778 667 722 833 611 556 833 833 389 389 778 611 1000 833 +833 611 833 722 611 667 778 778 1000 667 667 667 333 606 333 606 +500 333 500 611 444 611 500 389 556 611 333 333 611 333 889 611 +556 611 611 389 444 333 611 556 833 500 556 500 310 606 310 606 +750 500 750 333 500 500 1000 500 500 333 1000 611 389 1000 750 750 +750 750 278 278 500 500 606 500 1000 333 998 444 389 833 750 750 +667 250 278 500 500 606 500 606 500 333 747 438 500 606 333 747 +500 400 549 361 361 333 576 641 250 333 361 488 500 889 890 889 +444 778 778 778 778 778 778 1000 722 611 611 611 611 389 389 389 +389 833 833 833 833 833 833 833 606 833 778 778 778 778 667 611 +611 500 500 500 500 500 500 778 444 500 500 500 500 333 333 333 +333 556 611 556 556 556 556 556 549 556 611 611 611 611 556 611 +556 ] +/Encoding /WinAnsiEncoding +/FontDescriptor 8 0 R +>> +endobj +8 0 obj +<< +/Type /FontDescriptor +/FontName /BookAntiqua,Bold +/Flags 16418 +/FontBBox [ -250 -260 1236 930 ] +/MissingWidth 750 +/StemV 146 +/StemH 146 +/ItalicAngle 0 +/CapHeight 930 +/XHeight 651 +/Ascent 930 +/Descent 260 +/Leading 210 +/MaxWidth 1030 +/AvgWidth 460 +>> +endobj +2 0 obj +[ /PDF /Text ] +endobj +5 0 obj +<< +/Kids [4 0 R ] +/Count 1 +/Type /Pages +/MediaBox [ 0 0 612 792 ] +>> +endobj +1 0 obj +<< +/Creator (1725.fm) +/CreationDate (1-Jan-3 18:15PM) +/Title (1725.PDF) +/Author (Unknown) +/Producer (Acrobat PDFWriter 3.02 for Windows) +/Keywords () +/Subject () +>> +endobj +3 0 obj +<< +/Pages 5 0 R +/Type /Catalog +/DefaultGray 11 0 R +/DefaultRGB 12 0 R +>> +endobj +11 0 obj +[/CalGray +<< +/WhitePoint [0.9505 1 1.0891 ] +/Gamma 0.2468 +>> +] +endobj +12 0 obj +[/CalRGB +<< +/WhitePoint [0.9505 1 1.0891 ] +/Gamma [0.2468 0.2468 0.2468 ] +/Matrix [0.4361 0.2225 0.0139 0.3851 0.7169 0.0971 0.1431 0.0606 0.7141 ] +>> +] +endobj +xref +0 13 +0000000000 65535 f +0000002172 00000 n +0000002046 00000 n +0000002363 00000 n +0000000375 00000 n +0000002080 00000 n +0000000518 00000 n +0000000633 00000 n +0000001760 00000 n +0000000021 00000 n +0000000352 00000 n +0000002460 00000 n +0000002548 00000 n +trailer +<< +/Size 13 +/Root 3 0 R +/Info 1 0 R +/ID [<47149510433dd4882f05f8c124223734><47149510433dd4882f05f8c124223734>] +>> +startxref +2726 +%%EOF diff --git a/components/net/tests/parsable_mime/application/postscript/test.ps b/components/shared/net/tests/parsable_mime/application/postscript/test.ps index c273ffa3f0f..c273ffa3f0f 100755 --- a/components/net/tests/parsable_mime/application/postscript/test.ps +++ b/components/shared/net/tests/parsable_mime/application/postscript/test.ps diff --git a/components/net/tests/parsable_mime/application/vnd.ms-fontobject/vnd.ms-fontobject b/components/shared/net/tests/parsable_mime/application/vnd.ms-fontobject/vnd.ms-fontobject Binary files differindex 1b84f4c37c1..1b84f4c37c1 100755 --- a/components/net/tests/parsable_mime/application/vnd.ms-fontobject/vnd.ms-fontobject +++ b/components/shared/net/tests/parsable_mime/application/vnd.ms-fontobject/vnd.ms-fontobject diff --git a/components/net/tests/parsable_mime/application/x-gzip/test.gz b/components/shared/net/tests/parsable_mime/application/x-gzip/test.gz Binary files differindex 3b99b73e6f0..3b99b73e6f0 100644 --- a/components/net/tests/parsable_mime/application/x-gzip/test.gz +++ b/components/shared/net/tests/parsable_mime/application/x-gzip/test.gz diff --git a/components/net/tests/parsable_mime/application/x-rar-compressed/test.rar b/components/shared/net/tests/parsable_mime/application/x-rar-compressed/test.rar Binary files differindex 920bd4d8a8c..920bd4d8a8c 100755 --- a/components/net/tests/parsable_mime/application/x-rar-compressed/test.rar +++ b/components/shared/net/tests/parsable_mime/application/x-rar-compressed/test.rar diff --git a/components/net/tests/parsable_mime/application/zip/test.zip b/components/shared/net/tests/parsable_mime/application/zip/test.zip index 5c74c9658c6..5c74c9658c6 100755 --- a/components/net/tests/parsable_mime/application/zip/test.zip +++ b/components/shared/net/tests/parsable_mime/application/zip/test.zip diff --git a/components/net/tests/parsable_mime/audio/aiff/test.aif b/components/shared/net/tests/parsable_mime/audio/aiff/test.aif Binary files differindex ad2e35df40b..ad2e35df40b 100644 --- a/components/net/tests/parsable_mime/audio/aiff/test.aif +++ b/components/shared/net/tests/parsable_mime/audio/aiff/test.aif diff --git a/components/net/tests/parsable_mime/audio/basic/test.au b/components/shared/net/tests/parsable_mime/audio/basic/test.au Binary files differindex d4e53deb74a..d4e53deb74a 100644 --- a/components/net/tests/parsable_mime/audio/basic/test.au +++ b/components/shared/net/tests/parsable_mime/audio/basic/test.au diff --git a/components/net/tests/parsable_mime/audio/midi/test.mid b/components/shared/net/tests/parsable_mime/audio/midi/test.mid Binary files differindex a52838c62bc..a52838c62bc 100644 --- a/components/net/tests/parsable_mime/audio/midi/test.mid +++ b/components/shared/net/tests/parsable_mime/audio/midi/test.mid diff --git a/components/net/tests/parsable_mime/audio/mpeg/test.mp3 b/components/shared/net/tests/parsable_mime/audio/mpeg/test.mp3 Binary files differindex 50786790311..50786790311 100644 --- a/components/net/tests/parsable_mime/audio/mpeg/test.mp3 +++ b/components/shared/net/tests/parsable_mime/audio/mpeg/test.mp3 diff --git a/components/net/tests/parsable_mime/audio/wave/test.wav b/components/shared/net/tests/parsable_mime/audio/wave/test.wav Binary files differindex f96276c063c..f96276c063c 100644 --- a/components/net/tests/parsable_mime/audio/wave/test.wav +++ b/components/shared/net/tests/parsable_mime/audio/wave/test.wav diff --git a/components/net/tests/parsable_mime/image/bmp/test.bmp b/components/shared/net/tests/parsable_mime/image/bmp/test.bmp Binary files differindex 8a1b10bae5e..8a1b10bae5e 100644 --- a/components/net/tests/parsable_mime/image/bmp/test.bmp +++ b/components/shared/net/tests/parsable_mime/image/bmp/test.bmp diff --git a/components/net/tests/parsable_mime/image/gif/test87a b/components/shared/net/tests/parsable_mime/image/gif/test87a Binary files differindex 8d49c776420..8d49c776420 100644 --- a/components/net/tests/parsable_mime/image/gif/test87a +++ b/components/shared/net/tests/parsable_mime/image/gif/test87a diff --git a/components/net/tests/parsable_mime/image/gif/test89a.gif b/components/shared/net/tests/parsable_mime/image/gif/test89a.gif Binary files differindex 0e2995e0821..0e2995e0821 100644 --- a/components/net/tests/parsable_mime/image/gif/test89a.gif +++ b/components/shared/net/tests/parsable_mime/image/gif/test89a.gif diff --git a/components/net/tests/parsable_mime/image/jpeg/test.jpg b/components/shared/net/tests/parsable_mime/image/jpeg/test.jpg Binary files differindex 7f758f65d13..7f758f65d13 100644 --- a/components/net/tests/parsable_mime/image/jpeg/test.jpg +++ b/components/shared/net/tests/parsable_mime/image/jpeg/test.jpg diff --git a/components/net/tests/parsable_mime/image/png/test.png b/components/shared/net/tests/parsable_mime/image/png/test.png Binary files differindex cc81374d4f5..cc81374d4f5 100644 --- a/components/net/tests/parsable_mime/image/png/test.png +++ b/components/shared/net/tests/parsable_mime/image/png/test.png diff --git a/components/net/tests/parsable_mime/image/webp/test.webp b/components/shared/net/tests/parsable_mime/image/webp/test.webp Binary files differindex ad88e62f94c..ad88e62f94c 100755 --- a/components/net/tests/parsable_mime/image/webp/test.webp +++ b/components/shared/net/tests/parsable_mime/image/webp/test.webp diff --git a/components/net/tests/parsable_mime/image/x-icon/test.ico b/components/shared/net/tests/parsable_mime/image/x-icon/test.ico Binary files differindex a2d0ee49098..a2d0ee49098 100644 --- a/components/net/tests/parsable_mime/image/x-icon/test.ico +++ b/components/shared/net/tests/parsable_mime/image/x-icon/test.ico diff --git a/components/net/tests/parsable_mime/image/x-icon/test_cursor.ico b/components/shared/net/tests/parsable_mime/image/x-icon/test_cursor.ico Binary files differindex 6029d6684d5..6029d6684d5 100644 --- a/components/net/tests/parsable_mime/image/x-icon/test_cursor.ico +++ b/components/shared/net/tests/parsable_mime/image/x-icon/test_cursor.ico diff --git a/components/net/tests/parsable_mime/text/html/text_html_a_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_a_20.html index 1b9619279a7..1b9619279a7 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_a_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_a_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_a_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_a_20_u.html index 887a1280650..887a1280650 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_a_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_a_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_a_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_a_3e.html index 610cd08fea4..610cd08fea4 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_a_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_a_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_a_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_a_3e_u.html index 48528fdf341..48528fdf341 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_a_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_a_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_b_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_b_20.html index 5977d2eeaab..5977d2eeaab 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_b_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_b_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_b_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_b_20_u.html index a8a963b95f0..a8a963b95f0 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_b_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_b_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_b_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_b_3e.html index dc79cd5d568..dc79cd5d568 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_b_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_b_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_b_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_b_3e_u.html index 0d72d281aec..0d72d281aec 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_b_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_b_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_body_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_body_20.html index c72b1ad3bec..c72b1ad3bec 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_body_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_body_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_body_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_body_20_u.html index 1d76ebb0f47..1d76ebb0f47 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_body_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_body_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_body_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_body_3e.html index 4a66f59ef95..4a66f59ef95 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_body_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_body_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_body_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_body_3e_u.html index b431695f3ab..b431695f3ab 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_body_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_body_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_br_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_br_20.html index d04df680012..d04df680012 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_br_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_br_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_br_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_br_20_u.html index 1c0f0ce13ba..1c0f0ce13ba 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_br_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_br_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_br_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_br_3e.html index 1d40ef06566..1d40ef06566 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_br_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_br_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_br_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_br_3e_u.html index c4eae116f03..c4eae116f03 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_br_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_br_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_comment_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_20.html index e9612efc73c..e9612efc73c 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_comment_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_comment_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_20_u.html index e9612efc73c..e9612efc73c 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_comment_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_comment_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_3e.html index 44a94ca5a7a..44a94ca5a7a 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_comment_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_comment_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_3e_u.html index 44a94ca5a7a..44a94ca5a7a 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_comment_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_comment_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_div_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_div_20.html index 2ed34363b2f..2ed34363b2f 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_div_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_div_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_div_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_div_20_u.html index b98886efd83..b98886efd83 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_div_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_div_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_div_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_div_3e.html index ccf4ca8d70a..ccf4ca8d70a 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_div_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_div_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_div_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_div_3e_u.html index c117f0f4cdd..c117f0f4cdd 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_div_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_div_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_doctype_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_20.html index dbeb5a41c2a..dbeb5a41c2a 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_doctype_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_doctype_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_20_u.html index acede44dffb..acede44dffb 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_doctype_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_doctype_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_3e.html index 6a22ea8b978..6a22ea8b978 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_doctype_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_doctype_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_3e_u.html index 8b16e40458e..8b16e40458e 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_doctype_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_doctype_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_font_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_font_20.html index a18fa850617..a18fa850617 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_font_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_font_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_font_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_font_20_u.html index 6a31d2a8aba..6a31d2a8aba 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_font_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_font_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_font_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_font_3e.html index 3605840fc5a..3605840fc5a 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_font_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_font_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_font_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_font_3e_u.html index 1181517947b..1181517947b 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_font_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_font_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_h1_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_20.html index 3ed0eb125ff..3ed0eb125ff 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_h1_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_h1_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_20_u.html index f517b61487e..f517b61487e 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_h1_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_h1_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_3e.html index af0bf8c56b9..af0bf8c56b9 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_h1_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_h1_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_3e_u.html index bae85229fcf..bae85229fcf 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_h1_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_h1_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_head_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_head_20.html index eb322c946e0..eb322c946e0 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_head_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_head_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_head_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_head_20_u.html index 899655a5a2c..899655a5a2c 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_head_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_head_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_head_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_head_3e.html index 058c7dce4a9..058c7dce4a9 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_head_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_head_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_head_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_head_3e_u.html index 8a33d623daa..8a33d623daa 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_head_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_head_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_iframe_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_20.html index e632915590a..e632915590a 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_iframe_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_iframe_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_20_u.html index 527a06e415c..527a06e415c 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_iframe_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_iframe_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_3e.html index 9db0efd47d4..9db0efd47d4 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_iframe_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_iframe_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_3e_u.html index e3512c8a5c2..e3512c8a5c2 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_iframe_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_iframe_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_p_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_p_20.html index a099441be2b..a099441be2b 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_p_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_p_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_p_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_p_20_u.html index ff4befbde7b..ff4befbde7b 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_p_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_p_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_p_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_p_3e.html index 98db18913d0..98db18913d0 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_p_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_p_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_p_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_p_3e_u.html index 9d99a59ed08..9d99a59ed08 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_p_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_p_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_page_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_page_20.html index bb1c4572b25..bb1c4572b25 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_page_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_page_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_page_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_page_20_u.html index b3300d9f4e4..b3300d9f4e4 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_page_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_page_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_page_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_page_3e.html index e6a49c51924..e6a49c51924 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_page_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_page_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_page_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_page_3e_u.html index 2b8ee203d25..2b8ee203d25 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_page_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_page_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_script_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_script_20.html index 620c629266c..620c629266c 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_script_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_script_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_script_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_script_20_u.html index bd2c58e676c..bd2c58e676c 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_script_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_script_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_script_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_script_3e.html index d59535f70cd..d59535f70cd 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_script_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_script_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_script_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_script_3e_u.html index 625c13820f4..625c13820f4 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_script_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_script_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_style_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_style_20.html index 57bc2a1ecd7..57bc2a1ecd7 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_style_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_style_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_style_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_style_20_u.html index 8163eca3c60..8163eca3c60 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_style_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_style_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_style_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_style_3e.html index 229d5f951bd..229d5f951bd 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_style_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_style_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_style_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_style_3e_u.html index 12d686e4953..12d686e4953 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_style_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_style_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_table_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_table_20.html index 27cccc6acd6..27cccc6acd6 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_table_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_table_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_table_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_table_20_u.html index 556b46e7fc4..556b46e7fc4 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_table_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_table_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_table_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_table_3e.html index 351ee543af8..351ee543af8 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_table_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_table_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_table_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_table_3e_u.html index 6259870bfcf..6259870bfcf 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_table_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_table_3e_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_title_20.html b/components/shared/net/tests/parsable_mime/text/html/text_html_title_20.html index f7d151658d9..f7d151658d9 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_title_20.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_title_20.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_title_20_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_title_20_u.html index 03a072d8ac0..03a072d8ac0 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_title_20_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_title_20_u.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_title_3e.html b/components/shared/net/tests/parsable_mime/text/html/text_html_title_3e.html index fedf57b9e4f..fedf57b9e4f 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_title_3e.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_title_3e.html diff --git a/components/net/tests/parsable_mime/text/html/text_html_title_3e_u.html b/components/shared/net/tests/parsable_mime/text/html/text_html_title_3e_u.html index 5fbf8a75c47..5fbf8a75c47 100644 --- a/components/net/tests/parsable_mime/text/html/text_html_title_3e_u.html +++ b/components/shared/net/tests/parsable_mime/text/html/text_html_title_3e_u.html diff --git a/components/net/tests/parsable_mime/text/plain/utf16bebom.txt b/components/shared/net/tests/parsable_mime/text/plain/utf16bebom.txt Binary files differindex 9fb50d49fb8..9fb50d49fb8 100644 --- a/components/net/tests/parsable_mime/text/plain/utf16bebom.txt +++ b/components/shared/net/tests/parsable_mime/text/plain/utf16bebom.txt diff --git a/components/net/tests/parsable_mime/text/plain/utf16lebom.txt b/components/shared/net/tests/parsable_mime/text/plain/utf16lebom.txt Binary files differindex d79d81725ec..d79d81725ec 100644 --- a/components/net/tests/parsable_mime/text/plain/utf16lebom.txt +++ b/components/shared/net/tests/parsable_mime/text/plain/utf16lebom.txt diff --git a/components/net/tests/parsable_mime/text/plain/utf8bom.txt b/components/shared/net/tests/parsable_mime/text/plain/utf8bom.txt index 56ad8a265ef..56ad8a265ef 100644 --- a/components/net/tests/parsable_mime/text/plain/utf8bom.txt +++ b/components/shared/net/tests/parsable_mime/text/plain/utf8bom.txt diff --git a/components/net/tests/parsable_mime/text/xml/feed.atom b/components/shared/net/tests/parsable_mime/text/xml/feed.atom index 893c3f27a8e..893c3f27a8e 100755 --- a/components/net/tests/parsable_mime/text/xml/feed.atom +++ b/components/shared/net/tests/parsable_mime/text/xml/feed.atom diff --git a/components/net/tests/parsable_mime/text/xml/feed.rss b/components/shared/net/tests/parsable_mime/text/xml/feed.rss index 9dc94d32b51..57ea10d5b4e 100644 --- a/components/net/tests/parsable_mime/text/xml/feed.rss +++ b/components/shared/net/tests/parsable_mime/text/xml/feed.rss @@ -1,151 +1,151 @@ -<?xml version="1.0" encoding="windows-1252"?>
-<rss version="2.0">
- <channel>
- <title>FeedForAll Sample Feed</title>
- <description>RSS is a fascinating technology. The uses for RSS are expanding daily. Take a closer look at how various industries are using the benefits of RSS in their businesses.</description>
- <link>http://www.feedforall.com/industry-solutions.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <copyright>Copyright 2004 NotePage, Inc.</copyright>
- <docs>http://blogs.law.harvard.edu/tech/rss</docs>
- <language>en-us</language>
- <lastBuildDate>Tue, 19 Oct 2004 13:39:14 -0400</lastBuildDate>
- <managingEditor>marketing@feedforall.com</managingEditor>
- <pubDate>Tue, 19 Oct 2004 13:38:55 -0400</pubDate>
- <webMaster>webmaster@feedforall.com</webMaster>
- <generator>FeedForAll Beta1 (0.0.1.8)</generator>
- <image>
- <url>http://www.feedforall.com/ffalogo48x48.gif</url>
- <title>FeedForAll Sample Feed</title>
- <link>http://www.feedforall.com/industry-solutions.htm</link>
- <description>FeedForAll Sample Feed</description>
- <width>48</width>
- <height>48</height>
- </image>
- <item>
- <title>RSS Solutions for Restaurants</title>
- <description><b>FeedForAll </b>helps Restaurant's communicate with customers. Let your customers know the latest specials or events.<br>
-<br>
-RSS feed uses include:<br>
-<i><font color="#FF0000">Daily Specials <br>
-Entertainment <br>
-Calendar of Events </i></font></description>
- <link>http://www.feedforall.com/restaurant.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:09:11 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Schools and Colleges</title>
- <description>FeedForAll helps Educational Institutions communicate with students about school wide activities, events, and schedules.<br>
-<br>
-RSS feed uses include:<br>
-<i><font color="#0000FF">Homework Assignments <br>
-School Cancellations <br>
-Calendar of Events <br>
-Sports Scores <br>
-Clubs/Organization Meetings <br>
-Lunches Menus </i></font></description>
- <link>http://www.feedforall.com/schools.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:09:09 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Computer Service Companies</title>
- <description>FeedForAll helps Computer Service Companies communicate with clients about cyber security and related issues. <br>
-<br>
-Uses include:<br>
-<i><font color="#0000FF">Cyber Security Alerts <br>
-Specials<br>
-Job Postings </i></font></description>
- <link>http://www.feedforall.com/computer-service.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:09:07 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Governments</title>
- <description>FeedForAll helps Governments communicate with the general public about positions on various issues, and keep the community aware of changes in important legislative issues. <b><i><br>
-</b></i><br>
-RSS uses Include:<br>
-<i><font color="#00FF00">Legislative Calendar<br>
-Votes<br>
-Bulletins</i></font></description>
- <link>http://www.feedforall.com/government.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:09:05 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Politicians</title>
- <description>FeedForAll helps Politicians communicate with the general public about positions on various issues, and keep the community notified of their schedule. <br>
-<br>
-Uses Include:<br>
-<i><font color="#FF0000">Blogs<br>
-Speaking Engagements <br>
-Statements<br>
- </i></font></description>
- <link>http://www.feedforall.com/politics.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:09:03 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Meteorologists</title>
- <description>FeedForAll helps Meteorologists communicate with the general public about storm warnings and weather alerts, in specific regions. Using RSS meteorologists are able to quickly disseminate urgent and life threatening weather warnings. <br>
-<br>
-Uses Include:<br>
-<i><font color="#0000FF">Weather Alerts<br>
-Plotting Storms<br>
-School Cancellations </i></font></description>
- <link>http://www.feedforall.com/weather.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:09:01 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Realtors & Real Estate Firms</title>
- <description>FeedForAll helps Realtors and Real Estate companies communicate with clients informing them of newly available properties, and open house announcements. RSS helps to reach a targeted audience and spread the word in an inexpensive, professional manner. <font color="#0000FF"><br>
-</font><br>
-Feeds can be used for:<br>
-<i><font color="#FF0000">Open House Dates<br>
-New Properties For Sale<br>
-Mortgage Rates</i></font></description>
- <link>http://www.feedforall.com/real-estate.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:08:59 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Banks / Mortgage Companies</title>
- <description>FeedForAll helps <b>Banks, Credit Unions and Mortgage companies</b> communicate with the general public about rate changes in a prompt and professional manner. <br>
-<br>
-Uses include:<br>
-<i><font color="#0000FF">Mortgage Rates<br>
-Foreign Exchange Rates <br>
-Bank Rates<br>
-Specials</i></font></description>
- <link>http://www.feedforall.com/banks.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:08:57 -0400</pubDate>
- </item>
- <item>
- <title>RSS Solutions for Law Enforcement</title>
- <description><b>FeedForAll</b> helps Law Enforcement Professionals communicate with the general public and other agencies in a prompt and efficient manner. Using RSS police are able to quickly disseminate urgent and life threatening information. <br>
-<br>
-Uses include:<br>
-<i><font color="#0000FF">Amber Alerts<br>
-Sex Offender Community Notification <br>
-Weather Alerts <br>
-Scheduling <br>
-Security Alerts <br>
-Police Report <br>
-Meetings</i></font></description>
- <link>http://www.feedforall.com/law-enforcement.htm</link>
- <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category>
- <comments>http://www.feedforall.com/forum</comments>
- <pubDate>Tue, 19 Oct 2004 11:08:56 -0400</pubDate>
- </item>
- </channel>
+<?xml version="1.0" encoding="windows-1252"?> +<rss version="2.0"> + <channel> + <title>FeedForAll Sample Feed</title> + <description>RSS is a fascinating technology. The uses for RSS are expanding daily. Take a closer look at how various industries are using the benefits of RSS in their businesses.</description> + <link>http://www.feedforall.com/industry-solutions.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <copyright>Copyright 2004 NotePage, Inc.</copyright> + <docs>http://blogs.law.harvard.edu/tech/rss</docs> + <language>en-us</language> + <lastBuildDate>Tue, 19 Oct 2004 13:39:14 -0400</lastBuildDate> + <managingEditor>marketing@feedforall.com</managingEditor> + <pubDate>Tue, 19 Oct 2004 13:38:55 -0400</pubDate> + <webMaster>webmaster@feedforall.com</webMaster> + <generator>FeedForAll Beta1 (0.0.1.8)</generator> + <image> + <url>http://www.feedforall.com/ffalogo48x48.gif</url> + <title>FeedForAll Sample Feed</title> + <link>http://www.feedforall.com/industry-solutions.htm</link> + <description>FeedForAll Sample Feed</description> + <width>48</width> + <height>48</height> + </image> + <item> + <title>RSS Solutions for Restaurants</title> + <description><b>FeedForAll </b>helps Restaurant's communicate with customers. Let your customers know the latest specials or events.<br> +<br> +RSS feed uses include:<br> +<i><font color="#FF0000">Daily Specials <br> +Entertainment <br> +Calendar of Events </i></font></description> + <link>http://www.feedforall.com/restaurant.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:09:11 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Schools and Colleges</title> + <description>FeedForAll helps Educational Institutions communicate with students about school wide activities, events, and schedules.<br> +<br> +RSS feed uses include:<br> +<i><font color="#0000FF">Homework Assignments <br> +School Cancellations <br> +Calendar of Events <br> +Sports Scores <br> +Clubs/Organization Meetings <br> +Lunches Menus </i></font></description> + <link>http://www.feedforall.com/schools.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:09:09 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Computer Service Companies</title> + <description>FeedForAll helps Computer Service Companies communicate with clients about cyber security and related issues. <br> +<br> +Uses include:<br> +<i><font color="#0000FF">Cyber Security Alerts <br> +Specials<br> +Job Postings </i></font></description> + <link>http://www.feedforall.com/computer-service.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:09:07 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Governments</title> + <description>FeedForAll helps Governments communicate with the general public about positions on various issues, and keep the community aware of changes in important legislative issues. <b><i><br> +</b></i><br> +RSS uses Include:<br> +<i><font color="#00FF00">Legislative Calendar<br> +Votes<br> +Bulletins</i></font></description> + <link>http://www.feedforall.com/government.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:09:05 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Politicians</title> + <description>FeedForAll helps Politicians communicate with the general public about positions on various issues, and keep the community notified of their schedule. <br> +<br> +Uses Include:<br> +<i><font color="#FF0000">Blogs<br> +Speaking Engagements <br> +Statements<br> + </i></font></description> + <link>http://www.feedforall.com/politics.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:09:03 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Meteorologists</title> + <description>FeedForAll helps Meteorologists communicate with the general public about storm warnings and weather alerts, in specific regions. Using RSS meteorologists are able to quickly disseminate urgent and life threatening weather warnings. <br> +<br> +Uses Include:<br> +<i><font color="#0000FF">Weather Alerts<br> +Plotting Storms<br> +School Cancellations </i></font></description> + <link>http://www.feedforall.com/weather.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:09:01 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Realtors & Real Estate Firms</title> + <description>FeedForAll helps Realtors and Real Estate companies communicate with clients informing them of newly available properties, and open house announcements. RSS helps to reach a targeted audience and spread the word in an inexpensive, professional manner. <font color="#0000FF"><br> +</font><br> +Feeds can be used for:<br> +<i><font color="#FF0000">Open House Dates<br> +New Properties For Sale<br> +Mortgage Rates</i></font></description> + <link>http://www.feedforall.com/real-estate.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:08:59 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Banks / Mortgage Companies</title> + <description>FeedForAll helps <b>Banks, Credit Unions and Mortgage companies</b> communicate with the general public about rate changes in a prompt and professional manner. <br> +<br> +Uses include:<br> +<i><font color="#0000FF">Mortgage Rates<br> +Foreign Exchange Rates <br> +Bank Rates<br> +Specials</i></font></description> + <link>http://www.feedforall.com/banks.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:08:57 -0400</pubDate> + </item> + <item> + <title>RSS Solutions for Law Enforcement</title> + <description><b>FeedForAll</b> helps Law Enforcement Professionals communicate with the general public and other agencies in a prompt and efficient manner. Using RSS police are able to quickly disseminate urgent and life threatening information. <br> +<br> +Uses include:<br> +<i><font color="#0000FF">Amber Alerts<br> +Sex Offender Community Notification <br> +Weather Alerts <br> +Scheduling <br> +Security Alerts <br> +Police Report <br> +Meetings</i></font></description> + <link>http://www.feedforall.com/law-enforcement.htm</link> + <category domain="www.dmoz.com">Computers/Software/Internet/Site Management/Content Management</category> + <comments>http://www.feedforall.com/forum</comments> + <pubDate>Tue, 19 Oct 2004 11:08:56 -0400</pubDate> + </item> + </channel> </rss>
\ No newline at end of file diff --git a/components/net/tests/parsable_mime/text/xml/rdf_rss.xml b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss.xml index 4c58f82974e..4c58f82974e 100644 --- a/components/net/tests/parsable_mime/text/xml/rdf_rss.xml +++ b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss.xml diff --git a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_1.xml b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_1.xml index f6e486c5960..f6e486c5960 100644 --- a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_1.xml +++ b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_1.xml diff --git a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_2.xml b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_2.xml index be8414382e5..be8414382e5 100644 --- a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_2.xml +++ b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_2.xml diff --git a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_3.xml b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_3.xml index 5f0f03f1e2d..5f0f03f1e2d 100644 --- a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_3.xml +++ b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_3.xml diff --git a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_4.xml b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_4.xml index c06a80cf1f8..c06a80cf1f8 100644 --- a/components/net/tests/parsable_mime/text/xml/rdf_rss_ko_4.xml +++ b/components/shared/net/tests/parsable_mime/text/xml/rdf_rss_ko_4.xml diff --git a/components/net/tests/parsable_mime/text/xml/test.xml b/components/shared/net/tests/parsable_mime/text/xml/test.xml index 8fe8c0e91cf..8fe8c0e91cf 100644 --- a/components/net/tests/parsable_mime/text/xml/test.xml +++ b/components/shared/net/tests/parsable_mime/text/xml/test.xml diff --git a/components/net/tests/parsable_mime/unknown/binary_file b/components/shared/net/tests/parsable_mime/unknown/binary_file Binary files differindex ecf3bbcdf3e..ecf3bbcdf3e 100644 --- a/components/net/tests/parsable_mime/unknown/binary_file +++ b/components/shared/net/tests/parsable_mime/unknown/binary_file diff --git a/components/net/tests/parsable_mime/unknown/open_type b/components/shared/net/tests/parsable_mime/unknown/open_type index 9117b12600f..9117b12600f 100644 --- a/components/net/tests/parsable_mime/unknown/open_type +++ b/components/shared/net/tests/parsable_mime/unknown/open_type diff --git a/components/net/tests/parsable_mime/unknown/true_type.ttf b/components/shared/net/tests/parsable_mime/unknown/true_type.ttf Binary files differindex c0142fea093..c0142fea093 100644 --- a/components/net/tests/parsable_mime/unknown/true_type.ttf +++ b/components/shared/net/tests/parsable_mime/unknown/true_type.ttf diff --git a/components/net/tests/parsable_mime/unknown/true_type_collection.ttc b/components/shared/net/tests/parsable_mime/unknown/true_type_collection.ttc index 42d3cef1e6d..42d3cef1e6d 100644 --- a/components/net/tests/parsable_mime/unknown/true_type_collection.ttc +++ b/components/shared/net/tests/parsable_mime/unknown/true_type_collection.ttc diff --git a/components/net/tests/parsable_mime/video/avi/test.avi b/components/shared/net/tests/parsable_mime/video/avi/test.avi Binary files differindex f6cd837a924..f6cd837a924 100644 --- a/components/net/tests/parsable_mime/video/avi/test.avi +++ b/components/shared/net/tests/parsable_mime/video/avi/test.avi diff --git a/components/net/tests/parsable_mime/video/mp4/test.mp4 b/components/shared/net/tests/parsable_mime/video/mp4/test.mp4 Binary files differindex 1fc478842f5..1fc478842f5 100644 --- a/components/net/tests/parsable_mime/video/mp4/test.mp4 +++ b/components/shared/net/tests/parsable_mime/video/mp4/test.mp4 diff --git a/components/net/tests/parsable_mime/video/webm/test.webm b/components/shared/net/tests/parsable_mime/video/webm/test.webm Binary files differindex da946da5290..da946da5290 100644 --- a/components/net/tests/parsable_mime/video/webm/test.webm +++ b/components/shared/net/tests/parsable_mime/video/webm/test.webm diff --git a/components/shared/snapshot/lib.rs b/components/shared/snapshot/lib.rs index 7f0111f7a79..590de3666ad 100644 --- a/components/shared/snapshot/lib.rs +++ b/components/shared/snapshot/lib.rs @@ -87,7 +87,7 @@ pub type IpcSnapshot = Snapshot<IpcSharedMemory>; /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-get-a-copy-of-the-image-contents-of-a-context> #[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] pub struct Snapshot<T = Data> { - size: Size2D<u64>, + size: Size2D<u32>, /// internal data (can be any format it will be converted on use if needed) data: T, /// RGBA/BGRA (reflect internal data) @@ -97,7 +97,7 @@ pub struct Snapshot<T = Data> { } impl<T> Snapshot<T> { - pub const fn size(&self) -> Size2D<u64> { + pub const fn size(&self) -> Size2D<u32> { self.size } @@ -131,7 +131,7 @@ impl Snapshot<Data> { } /// Returns snapshot with provided size that is black transparent alpha - pub fn cleared(size: Size2D<u64>) -> Self { + pub fn cleared(size: Size2D<u32>) -> Self { Self { size, data: Data::Owned(vec![0; size.area() as usize * 4]), @@ -143,7 +143,7 @@ impl Snapshot<Data> { } pub fn from_vec( - size: Size2D<u64>, + size: Size2D<u32>, format: PixelFormat, alpha_mode: AlphaMode, data: Vec<u8>, @@ -157,7 +157,7 @@ impl Snapshot<Data> { } pub fn from_shared_memory( - size: Size2D<u64>, + size: Size2D<u32>, format: PixelFormat, alpha_mode: AlphaMode, ism: IpcSharedMemory, @@ -177,7 +177,7 @@ impl Snapshot<Data> { /// This is safe if data is owned by this process only /// (ownership is transferred on send) pub unsafe fn from_shared_memory( - size: Size2D<u64>, + size: Size2D<u32>, format: PixelFormat, alpha_mode: AlphaMode, ism: IpcSharedMemory, diff --git a/components/webdriver_server/actions.rs b/components/webdriver_server/actions.rs index f33310ac001..774ed41e3cb 100644 --- a/components/webdriver_server/actions.rs +++ b/components/webdriver_server/actions.rs @@ -2,9 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use std::thread; use std::time::{Duration, Instant}; -use std::{cmp, thread}; use constellation_traits::EmbedderToConstellationMessage; use embedder_traits::{MouseButtonAction, WebDriverCommandMsg, WebDriverScriptCommand}; @@ -12,10 +12,11 @@ use ipc_channel::ipc; use keyboard_types::webdriver::KeyInputState; use webdriver::actions::{ ActionSequence, ActionsType, GeneralAction, KeyAction, KeyActionItem, KeyDownAction, - KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerActionParameters, - PointerDownAction, PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, - WheelActionItem, WheelScrollAction, + KeyUpAction, NullActionItem, PointerAction, PointerActionItem, PointerDownAction, + PointerMoveAction, PointerOrigin, PointerType, PointerUpAction, WheelAction, WheelActionItem, + WheelScrollAction, }; +use webdriver::command::ActionsParameters; use webdriver::error::{ErrorStatus, WebDriverError}; use crate::{Handler, WebElement, wait_for_script_response}; @@ -24,11 +25,32 @@ use crate::{Handler, WebElement, wait_for_script_response}; static POINTERMOVE_INTERVAL: u64 = 17; static WHEELSCROLL_INTERVAL: u64 = 17; -// https://w3c.github.io/webdriver/#dfn-input-source-state +// A single action, corresponding to an `action object` in the spec. +// In the spec, `action item` refers to a plain JSON object. +// However, we use the name ActionItem here +// to be consistent with type names from webdriver crate. +pub(crate) enum ActionItem { + Null(NullActionItem), + Key(KeyActionItem), + Pointer(PointerActionItem), + Wheel(WheelActionItem), +} + +// A set of actions with multiple sources executed within a single tick. +// The order in which they are performed is not guaranteed. +// The `id` is used to identify the source of the actions. +pub(crate) type TickActions = HashMap<String, ActionItem>; + +// Consumed by the `dispatch_actions` method. +pub(crate) type ActionsByTick = Vec<TickActions>; + +/// <https://w3c.github.io/webdriver/#dfn-input-source-state> pub(crate) enum InputSourceState { Null, + #[allow(dead_code)] Key(KeyInputState), Pointer(PointerInputState), + #[allow(dead_code)] Wheel, } @@ -44,13 +66,9 @@ pub(crate) struct PointerInputState { } impl PointerInputState { - pub fn new(subtype: &PointerType) -> PointerInputState { + pub fn new(subtype: PointerType) -> PointerInputState { PointerInputState { - subtype: match subtype { - PointerType::Mouse => PointerType::Mouse, - PointerType::Pen => PointerType::Pen, - PointerType::Touch => PointerType::Touch, - }, + subtype, pressed: HashSet::new(), x: 0.0, y: 0.0, @@ -58,48 +76,44 @@ impl PointerInputState { } } -// https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration -fn compute_tick_duration(tick_actions: &ActionSequence) -> u64 { - let mut duration = 0; - match &tick_actions.actions { - ActionsType::Null { actions } => { - for action in actions.iter() { - let NullActionItem::General(GeneralAction::Pause(pause_action)) = action; - duration = cmp::max(duration, pause_action.duration.unwrap_or(0)); - } - }, - ActionsType::Pointer { - parameters: _, - actions, - } => { - for action in actions.iter() { - let action_duration = match action { - PointerActionItem::General(GeneralAction::Pause(action)) => action.duration, - PointerActionItem::Pointer(PointerAction::Move(action)) => action.duration, - _ => None, - }; - duration = cmp::max(duration, action_duration.unwrap_or(0)); - } - }, - ActionsType::Key { actions: _ } => (), - ActionsType::Wheel { actions } => { - for action in actions.iter() { - let action_duration = match action { - WheelActionItem::General(GeneralAction::Pause(action)) => action.duration, - WheelActionItem::Wheel(WheelAction::Scroll(action)) => action.duration, - }; - duration = cmp::max(duration, action_duration.unwrap_or(0)); +/// <https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration> +fn compute_tick_duration(tick_actions: &TickActions) -> u64 { + // Step 1. Let max duration be 0. + // Step 2. For each action in tick actions: + tick_actions + .iter() + .filter_map(|(_, action_item)| { + // If action object has subtype property set to "pause" or + // action object has type property set to "pointer" and subtype property set to "pointerMove", + // or action object has type property set to "wheel" and subtype property set to "scroll", + // let duration be equal to the duration property of action object. + match action_item { + ActionItem::Null(NullActionItem::General(GeneralAction::Pause(pause_action))) | + ActionItem::Key(KeyActionItem::General(GeneralAction::Pause(pause_action))) | + ActionItem::Pointer(PointerActionItem::General(GeneralAction::Pause( + pause_action, + ))) | + ActionItem::Wheel(WheelActionItem::General(GeneralAction::Pause(pause_action))) => { + pause_action.duration + }, + ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move(action))) => { + action.duration + }, + ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(action))) => { + action.duration + }, + _ => None, } - }, - } - duration + }) + .max() + .unwrap_or(0) } impl Handler { - // https://w3c.github.io/webdriver/#dfn-dispatch-actions + /// <https://w3c.github.io/webdriver/#dfn-dispatch-actions> pub(crate) fn dispatch_actions( &self, - actions_by_tick: &[ActionSequence], + actions_by_tick: ActionsByTick, ) -> Result<(), ErrorStatus> { // Step 1. Wait for an action queue token with input state. let new_token = self.id_generator.next(); @@ -116,11 +130,8 @@ impl Handler { res } - // https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner - fn dispatch_actions_inner( - &self, - actions_by_tick: &[ActionSequence], - ) -> Result<(), ErrorStatus> { + /// <https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner> + fn dispatch_actions_inner(&self, actions_by_tick: ActionsByTick) -> Result<(), ErrorStatus> { // Step 1. For each item tick actions in actions by tick for tick_actions in actions_by_tick.iter() { // Step 1.2. Let tick duration be the result of @@ -133,25 +144,43 @@ impl Handler { // Step 1.4. Wait for // The user agent event loop has spun enough times to process the DOM events // generated by the last invocation of the dispatch tick actions steps. - // - // To ensure we wait for all events to be processed, only the last event in - // this tick action step holds the message id. - // Whenever a new event is generated, the message id is passed to it. - // - // TO-DO: remove the first match after webdriver_id is implemented in all commands - match tick_actions.actions { - ActionsType::Key { .. } | ActionsType::Wheel { .. } | ActionsType::Null { .. } => { - return Ok(()); - }, - _ => {}, - } + self.wait_for_user_agent_handling_complete(tick_actions)?; + } + // Step 2. Return success with data null. + dbg!("Dispatch actions completed successfully"); + Ok(()) + } + + fn wait_for_user_agent_handling_complete( + &self, + tick_actions: &TickActions, + ) -> Result<(), ErrorStatus> { + // TODO: Add matches! for key actions + // after implmenting webdriver id for key events. + let count_non_null_actions_in_tick = tick_actions + .iter() + .filter(|(_, action)| { + matches!( + action, + ActionItem::Pointer(PointerActionItem::Pointer(_)) | + ActionItem::Wheel(WheelActionItem::Wheel(_)) + ) + }) + .count(); + + // To ensure we wait for all events to be processed, only the last event + // in each tick action step holds the message id. + // Whenever a new event is generated, the message id is passed to it. + // + // Wait for count_non_null_actions_in_tick number of responses + for _ in 0..count_non_null_actions_in_tick { match self.constellation_receiver.recv() { Ok(response) => { let current_waiting_id = self .current_action_id .get() - .expect("Current id should be set before dispat_actions_inner is called"); + .expect("Current id should be set before dispatch_actions_inner is called"); if current_waiting_id != response.id { dbg!("Dispatch actions completed with wrong id in response"); @@ -165,8 +194,86 @@ impl Handler { }; } - // Step 2. Return success with data null. - dbg!("Dispatch actions completed successfully"); + Ok(()) + } + + /// <https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions> + fn dispatch_tick_actions( + &self, + tick_actions: &TickActions, + tick_duration: u64, + ) -> Result<(), ErrorStatus> { + // Step 1. For each action object in tick actions: + // Step 1.1. Let input_id be the value of the id property of action object. + for (input_id, action) in tick_actions.iter() { + // Step 6. Let subtype be action object's subtype. + // Steps 7, 8. Try to run specific algorithm based on the action type. + match action { + ActionItem::Null(NullActionItem::General(_)) => { + self.dispatch_general_action(input_id); + }, + ActionItem::Key(KeyActionItem::General(_)) => { + self.dispatch_general_action(input_id); + }, + ActionItem::Key(KeyActionItem::Key(KeyAction::Down(keydown_action))) => { + self.dispatch_keydown_action(input_id, keydown_action); + + // Step 9. If subtype is "keyDown", append a copy of action + // object with the subtype property changed to "keyUp" to + // input state's input cancel list. + self.session() + .unwrap() + .input_cancel_list + .borrow_mut() + .push(ActionItem::Key(KeyActionItem::Key(KeyAction::Up( + KeyUpAction { + value: keydown_action.value.clone(), + }, + )))); + }, + ActionItem::Key(KeyActionItem::Key(KeyAction::Up(keyup_action))) => { + self.dispatch_keyup_action(input_id, keyup_action); + }, + ActionItem::Pointer(PointerActionItem::General(_)) => { + self.dispatch_general_action(input_id); + }, + ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Down( + pointer_down_action, + ))) => { + self.dispatch_pointerdown_action(input_id, pointer_down_action); + + // Step 10. If subtype is "pointerDown", append a copy of action + // object with the subtype property changed to "pointerUp" to + // input state's input cancel list. + self.session().unwrap().input_cancel_list.borrow_mut().push( + ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up( + PointerUpAction { + button: pointer_down_action.button, + ..Default::default() + }, + ))), + ); + }, + ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Move( + pointer_move_action, + ))) => { + self.dispatch_pointermove_action(input_id, pointer_move_action, tick_duration)?; + }, + ActionItem::Pointer(PointerActionItem::Pointer(PointerAction::Up( + pointer_up_action, + ))) => { + self.dispatch_pointerup_action(input_id, pointer_up_action); + }, + ActionItem::Wheel(WheelActionItem::General(_)) => { + self.dispatch_general_action(input_id); + }, + ActionItem::Wheel(WheelActionItem::Wheel(WheelAction::Scroll(scroll_action))) => { + self.dispatch_scroll_action(scroll_action, tick_duration)?; + }, + _ => {}, + } + } + Ok(()) } @@ -181,143 +288,7 @@ impl Handler { // Nothing to be done } - // https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions - fn dispatch_tick_actions( - &self, - tick_actions: &ActionSequence, - tick_duration: u64, - ) -> Result<(), ErrorStatus> { - let source_id = &tick_actions.id; - match &tick_actions.actions { - ActionsType::Null { actions } => { - for _action in actions.iter() { - self.dispatch_general_action(source_id); - } - }, - ActionsType::Key { actions } => { - for action in actions.iter() { - match action { - KeyActionItem::General(_action) => { - self.dispatch_general_action(source_id); - }, - KeyActionItem::Key(action) => { - self.session() - .unwrap() - .input_state_table - .borrow_mut() - .entry(source_id.to_string()) - .or_insert(InputSourceState::Key(KeyInputState::new())); - match action { - KeyAction::Down(action) => { - self.dispatch_keydown_action(source_id, action); - // Step 9. If subtype is "keyDown", append a copy of action - // object with the subtype property changed to "keyUp" to - // input state's input cancel list. - self.session().unwrap().input_cancel_list.borrow_mut().push( - ActionSequence { - id: source_id.into(), - actions: ActionsType::Key { - actions: vec![KeyActionItem::Key(KeyAction::Up( - KeyUpAction { - value: action.value.clone(), - }, - ))], - }, - }, - ); - }, - KeyAction::Up(action) => { - self.dispatch_keyup_action(source_id, action) - }, - }; - }, - } - } - }, - ActionsType::Pointer { - parameters, - actions, - } => { - for action in actions.iter() { - match action { - PointerActionItem::General(_action) => { - self.dispatch_general_action(source_id); - }, - PointerActionItem::Pointer(action) => { - self.session() - .unwrap() - .input_state_table - .borrow_mut() - .entry(source_id.to_string()) - .or_insert(InputSourceState::Pointer(PointerInputState::new( - ¶meters.pointer_type, - ))); - match action { - PointerAction::Cancel => (), - PointerAction::Down(action) => { - self.dispatch_pointerdown_action(source_id, action); - - // Step 10. If subtype is "pointerDown", append a copy of action - // object with the subtype property changed to "pointerUp" to - // input state's input cancel list. - self.session().unwrap().input_cancel_list.borrow_mut().push( - ActionSequence { - id: source_id.into(), - actions: ActionsType::Pointer { - parameters: PointerActionParameters { - pointer_type: parameters.pointer_type, - }, - actions: vec![PointerActionItem::Pointer( - PointerAction::Up(PointerUpAction { - button: action.button, - ..Default::default() - }), - )], - }, - }, - ); - }, - PointerAction::Move(action) => self.dispatch_pointermove_action( - source_id, - action, - tick_duration, - )?, - PointerAction::Up(action) => { - self.dispatch_pointerup_action(source_id, action) - }, - } - }, - } - } - }, - ActionsType::Wheel { actions } => { - for action in actions.iter() { - match action { - WheelActionItem::General(_action) => { - self.dispatch_general_action(source_id) - }, - WheelActionItem::Wheel(action) => { - self.session() - .unwrap() - .input_state_table - .borrow_mut() - .entry(source_id.to_string()) - .or_insert(InputSourceState::Wheel); - match action { - WheelAction::Scroll(action) => { - self.dispatch_scroll_action(action, tick_duration)? - }, - } - }, - } - } - }, - } - - Ok(()) - } - - // https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action + /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action> fn dispatch_keydown_action(&self, source_id: &str, action: &KeyDownAction) { let session = self.session().unwrap(); @@ -340,7 +311,7 @@ impl Handler { .unwrap(); } - // https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action + /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action> fn dispatch_keyup_action(&self, source_id: &str, action: &KeyUpAction) { let session = self.session().unwrap(); @@ -378,7 +349,7 @@ impl Handler { } pointer_input_state.pressed.insert(action.button); - let msg_id = self.current_action_id.get().unwrap(); + let msg_id = self.current_action_id.get(); let cmd_msg = WebDriverCommandMsg::MouseButtonAction( session.webview_id, MouseButtonAction::Down, @@ -393,7 +364,7 @@ impl Handler { .unwrap(); } - // https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action + /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action> pub(crate) fn dispatch_pointerup_action(&self, source_id: &str, action: &PointerUpAction) { let session = self.session().unwrap(); @@ -408,7 +379,7 @@ impl Handler { } pointer_input_state.pressed.remove(&action.button); - let msg_id = self.current_action_id.get().unwrap(); + let msg_id = self.current_action_id.get(); let cmd_msg = WebDriverCommandMsg::MouseButtonAction( session.webview_id, MouseButtonAction::Up, @@ -423,7 +394,7 @@ impl Handler { .unwrap(); } - // https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action + /// <https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action> pub(crate) fn dispatch_pointermove_action( &self, source_id: &str, @@ -529,9 +500,15 @@ impl Handler { let current_y = pointer_input_state.y; // Step 7 - if x != current_x || y != current_y { + // Actually "last" should not be checked here based on spec. + // However, we need to send the webdriver id at the final perform. + if x != current_x || y != current_y || last { // Step 7.2 - let msg_id = self.current_action_id.get().unwrap(); + let msg_id = if last { + self.current_action_id.get() + } else { + None + }; let cmd_msg = WebDriverCommandMsg::MouseMoveAction( session.webview_id, x as f32, @@ -660,14 +637,23 @@ impl Handler { }; // Step 5 - if delta_x != 0 || delta_y != 0 { + // Actually "last" should not be checked here based on spec. + // However, we need to send the webdriver id at the final perform. + if delta_x != 0 || delta_y != 0 || last { // Perform implementation-specific action dispatch steps + let msg_id = if last { + self.current_action_id.get() + } else { + None + }; let cmd_msg = WebDriverCommandMsg::WheelScrollAction( session.webview_id, x as f32, y as f32, delta_x as f64, delta_y as f64, + msg_id, + self.constellation_sender.clone(), ); self.constellation_chan .send(EmbedderToConstellationMessage::WebDriverCommand(cmd_msg)) @@ -738,4 +724,94 @@ impl Handler { None => Err(ErrorStatus::UnknownError), } } + + /// <https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence> + pub(crate) fn extract_an_action_sequence(&self, params: ActionsParameters) -> ActionsByTick { + // Step 1. Let actions be the result of getting a property named "actions" from parameters. + // Step 2 (ignored because params is already validated earlier). If actions is not a list, + // return an error with status InvalidArgument. + let actions = params.actions; + + self.actions_by_tick_from_sequence(actions) + } + + pub(crate) fn actions_by_tick_from_sequence( + &self, + actions: Vec<ActionSequence>, + ) -> ActionsByTick { + // Step 3. Let actions by tick be an empty list. + let mut actions_by_tick: ActionsByTick = Vec::new(); + + // Step 4. For each value action sequence corresponding to an indexed property in actions + for action_sequence in actions { + // Store id before moving action_sequence + let id = action_sequence.id.clone(); + // Step 4.1. Let source actions be the result of trying to process an input source action sequence + let source_actions = self.process_an_input_source_action_sequence(action_sequence); + + // Step 4.2.2. Ensure we have enough ticks to hold all actions + while actions_by_tick.len() < source_actions.len() { + actions_by_tick.push(HashMap::new()); + } + + // Step 4.2.3. + for (tick_index, action_item) in source_actions.into_iter().enumerate() { + actions_by_tick[tick_index].insert(id.clone(), action_item); + } + } + + actions_by_tick + } + + /// <https://w3c.github.io/webdriver/#dfn-process-an-input-source-action-sequence> + pub(crate) fn process_an_input_source_action_sequence( + &self, + action_sequence: ActionSequence, + ) -> Vec<ActionItem> { + // Step 2. Let id be the value of the id property of action sequence. + let id = action_sequence.id.clone(); + + let mut input_state_table = self.session().unwrap().input_state_table.borrow_mut(); + + match action_sequence.actions { + ActionsType::Null { + actions: null_actions, + } => { + input_state_table + .entry(id) + .or_insert(InputSourceState::Null); + null_actions.into_iter().map(ActionItem::Null).collect() + }, + ActionsType::Key { + actions: key_actions, + } => { + input_state_table + .entry(id) + .or_insert(InputSourceState::Key(KeyInputState::new())); + key_actions.into_iter().map(ActionItem::Key).collect() + }, + ActionsType::Pointer { + parameters: _, + actions: pointer_actions, + } => { + input_state_table + .entry(id) + .or_insert(InputSourceState::Pointer(PointerInputState::new( + PointerType::Mouse, + ))); + pointer_actions + .into_iter() + .map(ActionItem::Pointer) + .collect() + }, + ActionsType::Wheel { + actions: wheel_actions, + } => { + input_state_table + .entry(id) + .or_insert(InputSourceState::Wheel); + wheel_actions.into_iter().map(ActionItem::Wheel).collect() + }, + } + } } diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index eb3e7bf17ec..cd479d7fae6 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -24,9 +24,9 @@ use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection}; use cookie::{CookieBuilder, Expiration}; use crossbeam_channel::{Receiver, Sender, after, select, unbounded}; use embedder_traits::{ - MouseButton, WebDriverCommandMsg, WebDriverCommandResponse, WebDriverCookieError, - WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverJSValue, WebDriverLoadStatus, - WebDriverMessageId, WebDriverScriptCommand, + MouseButton, WebDriverCommandMsg, WebDriverCommandResponse, WebDriverFrameId, WebDriverJSError, + WebDriverJSResult, WebDriverJSValue, WebDriverLoadStatus, WebDriverMessageId, + WebDriverScriptCommand, }; use euclid::{Rect, Size2D}; use http::method::Method; @@ -64,7 +64,7 @@ use webdriver::response::{ }; use webdriver::server::{self, Session, SessionTeardownKind, WebDriverHandler}; -use crate::actions::{InputSourceState, PointerInputState}; +use crate::actions::{ActionItem, InputSourceState, PointerInputState}; #[derive(Default)] pub struct WebDriverMessageIdGenerator { @@ -171,7 +171,7 @@ pub struct WebDriverSession { input_state_table: RefCell<HashMap<String, InputSourceState>>, /// <https://w3c.github.io/webdriver/#dfn-input-cancel-list> - input_cancel_list: RefCell<Vec<ActionSequence>>, + input_cancel_list: RefCell<Vec<ActionItem>>, } impl WebDriverSession { @@ -667,6 +667,7 @@ impl Handler { cmd_msg: WebDriverScriptCommand, ) -> WebDriverResult<()> { let browsing_context_id = self.session()?.browsing_context_id; + self.verify_browsing_context_is_open(browsing_context_id)?; let msg = EmbedderToConstellationMessage::WebDriverCommand( WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd_msg), ); @@ -675,7 +676,9 @@ impl Handler { } fn top_level_script_command(&self, cmd_msg: WebDriverScriptCommand) -> WebDriverResult<()> { - let browsing_context_id = BrowsingContextId::from(self.session()?.webview_id); + let webview_id = self.session()?.webview_id; + self.verify_top_level_browsing_context_is_open(webview_id)?; + let browsing_context_id = BrowsingContextId::from(webview_id); let msg = EmbedderToConstellationMessage::WebDriverCommand( WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd_msg), ); @@ -683,7 +686,14 @@ impl Handler { Ok(()) } + /// <https://w3c.github.io/webdriver/#navigate-to> fn handle_get(&self, parameters: &GetParameters) -> WebDriverResult<WebDriverResponse> { + let webview_id = self.session()?.webview_id; + // Step 2. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(webview_id)?; + // Step 3. If URL is not an absolute URL or is not an absolute URL with fragment + // or not a local scheme, return error with error code invalid argument. let url = match ServoUrl::parse(¶meters.url[..]) { Ok(url) => url, Err(_) => { @@ -694,8 +704,6 @@ impl Handler { }, }; - let webview_id = self.session()?.webview_id; - let cmd_msg = WebDriverCommandMsg::LoadUrl(webview_id, url, self.load_status_sender.clone()); self.constellation_chan @@ -732,6 +740,9 @@ impl Handler { fn handle_window_size(&self) -> WebDriverResult<WebDriverResponse> { let (sender, receiver) = ipc::channel().unwrap(); let webview_id = self.session()?.webview_id; + + self.verify_top_level_browsing_context_is_open(webview_id)?; + let cmd_msg = WebDriverCommandMsg::GetWindowSize(webview_id, sender); self.constellation_chan @@ -764,6 +775,11 @@ impl Handler { let height = params.height.unwrap_or(0); let size = Size2D::new(width as u32, height as u32); let webview_id = self.session()?.webview_id; + // TODO: Return some other error earlier if the size is invalid. + + // Step 12. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(webview_id)?; let cmd_msg = WebDriverCommandMsg::SetWindowSize(webview_id, size.to_i32(), sender.clone()); self.constellation_chan @@ -793,6 +809,10 @@ impl Handler { } fn handle_is_enabled(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> { + // Step 1. If session's current browsing context is no longer open, + // return error with error code no such window. + self.verify_browsing_context_is_open(self.session()?.browsing_context_id)?; + let (sender, receiver) = ipc::channel().unwrap(); self.top_level_script_command(WebDriverScriptCommand::IsEnabled( @@ -809,6 +829,10 @@ impl Handler { } fn handle_is_selected(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> { + // Step 1. If session's current browsing context is no longer open, + // return error with error code no such window. + self.verify_browsing_context_is_open(self.session()?.browsing_context_id)?; + let (sender, receiver) = ipc::channel().unwrap(); self.top_level_script_command(WebDriverScriptCommand::IsSelected( @@ -826,6 +850,9 @@ impl Handler { fn handle_go_back(&self) -> WebDriverResult<WebDriverResponse> { let webview_id = self.session()?.webview_id; + // Step 1. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(webview_id)?; let direction = TraversalDirection::Back(1); let msg = EmbedderToConstellationMessage::TraverseHistory(webview_id, direction); self.constellation_chan.send(msg).unwrap(); @@ -834,6 +861,9 @@ impl Handler { fn handle_go_forward(&self) -> WebDriverResult<WebDriverResponse> { let webview_id = self.session()?.webview_id; + // Step 1. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(webview_id)?; let direction = TraversalDirection::Forward(1); let msg = EmbedderToConstellationMessage::TraverseHistory(webview_id, direction); self.constellation_chan.send(msg).unwrap(); @@ -842,6 +872,9 @@ impl Handler { fn handle_refresh(&self) -> WebDriverResult<WebDriverResponse> { let webview_id = self.session()?.webview_id; + // Step 1. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(webview_id)?; let cmd_msg = WebDriverCommandMsg::Refresh(webview_id, self.load_status_sender.clone()); self.constellation_chan @@ -864,6 +897,9 @@ impl Handler { fn handle_window_handle(&self) -> WebDriverResult<WebDriverResponse> { let session = self.session.as_ref().unwrap(); + // Step 1. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(session.webview_id)?; match session.window_handles.get(&session.webview_id) { Some(handle) => Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(handle)?, @@ -929,10 +965,13 @@ impl Handler { } } + /// <https://w3c.github.io/webdriver/#close-window> fn handle_close_window(&mut self) -> WebDriverResult<WebDriverResponse> { { + let webview_id = self.session()?.webview_id; + self.verify_top_level_browsing_context_is_open(webview_id)?; let session = self.session_mut().unwrap(); - session.window_handles.remove(&session.webview_id); + session.window_handles.remove(&webview_id); let cmd_msg = WebDriverCommandMsg::CloseWebView(session.webview_id); self.constellation_chan .send(EmbedderToConstellationMessage::WebDriverCommand(cmd_msg)) @@ -949,6 +988,7 @@ impl Handler { ))) } + /// <https://w3c.github.io/webdriver/#new-window> fn handle_new_window( &mut self, _parameters: &NewWindowParameters, @@ -956,11 +996,15 @@ impl Handler { let (sender, receiver) = ipc::channel().unwrap(); let session = self.session().unwrap(); + self.verify_top_level_browsing_context_is_open(session.webview_id)?; + let cmd_msg = WebDriverCommandMsg::NewWebView( session.webview_id, sender, self.load_status_sender.clone(), ); + // Step 5. Create a new top-level browsing context by running the window open steps. + // This MUST be done without invoking the focusing steps. self.constellation_chan .send(EmbedderToConstellationMessage::WebDriverCommand(cmd_msg)) .unwrap(); @@ -968,8 +1012,6 @@ impl Handler { let mut handle = self.session.as_ref().unwrap().id.to_string(); if let Ok(new_webview_id) = receiver.recv() { let session = self.session_mut().unwrap(); - session.webview_id = new_webview_id; - session.browsing_context_id = BrowsingContextId::from(new_webview_id); let new_handle = Uuid::new_v4().to_string(); handle = new_handle.clone(); session.window_handles.insert(new_webview_id, new_handle); @@ -1214,6 +1256,28 @@ impl Handler { } } + fn handle_get_shadow_root(&self, element: WebElement) -> WebDriverResult<WebDriverResponse> { + let (sender, receiver) = ipc::channel().unwrap(); + let cmd = WebDriverScriptCommand::GetElementShadowRoot(element.to_string(), sender); + self.browsing_context_script_command(cmd)?; + match wait_for_script_response(receiver)? { + Ok(value) => { + if value.is_none() { + return Err(WebDriverError::new( + ErrorStatus::NoSuchShadowRoot, + "No shadow root found for the element", + )); + } + let value_resp = serde_json::to_value( + value.map(|x| serde_json::to_value(WebElement(x)).unwrap()), + )?; + let shadow_root_value = json!({ SHADOW_ROOT_IDENTIFIER: value_resp }); + Ok(WebDriverResponse::Generic(ValueResponse(shadow_root_value))) + }, + Err(error) => Err(WebDriverError::new(error, "")), + } + } + // https://w3c.github.io/webdriver/webdriver-spec.html#get-element-rect fn handle_element_rect(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> { let (sender, receiver) = ipc::channel().unwrap(); @@ -1343,7 +1407,10 @@ impl Handler { let (sender, receiver) = ipc::channel().unwrap(); let cmd = WebDriverScriptCommand::GetCookies(sender); self.browsing_context_script_command(cmd)?; - let cookies = wait_for_script_response(receiver)?; + let cookies = match wait_for_script_response(receiver)? { + Ok(cookies) => cookies, + Err(error) => return Err(WebDriverError::new(error, "")), + }; let response = cookies .into_iter() .map(|cookie| cookie_msg_to_cookie(cookie.into_inner())) @@ -1355,7 +1422,10 @@ impl Handler { let (sender, receiver) = ipc::channel().unwrap(); let cmd = WebDriverScriptCommand::GetCookie(name, sender); self.browsing_context_script_command(cmd)?; - let cookies = wait_for_script_response(receiver)?; + let cookies = match wait_for_script_response(receiver)? { + Ok(cookies) => cookies, + Err(error) => return Err(WebDriverError::new(error, "")), + }; let Some(response) = cookies .into_iter() .map(|cookie| cookie_msg_to_cookie(cookie.into_inner())) @@ -1388,16 +1458,7 @@ impl Handler { self.browsing_context_script_command(cmd)?; match wait_for_script_response(receiver)? { Ok(_) => Ok(WebDriverResponse::Void), - Err(response) => match response { - WebDriverCookieError::InvalidDomain => Err(WebDriverError::new( - ErrorStatus::InvalidCookieDomain, - "Invalid cookie domain", - )), - WebDriverCookieError::UnableToSetCookie => Err(WebDriverError::new( - ErrorStatus::UnableToSetCookie, - "Unable to set cookie", - )), - }, + Err(error) => Err(WebDriverError::new(error, "")), } } @@ -1480,21 +1541,29 @@ impl Handler { fn handle_perform_actions( &mut self, - parameters: &ActionsParameters, + parameters: ActionsParameters, ) -> WebDriverResult<WebDriverResponse> { - match self.dispatch_actions(¶meters.actions) { + // Step 1. If session's current browsing context is no longer open, + // return error with error code no such window. + self.verify_browsing_context_is_open(self.session()?.browsing_context_id)?; + // Step 5. Let actions by tick be the result of trying to extract an action sequence + let actions_by_tick = self.extract_an_action_sequence(parameters); + + // Step 6. Dispatch actions + match self.dispatch_actions(actions_by_tick) { Ok(_) => Ok(WebDriverResponse::Void), Err(error) => Err(WebDriverError::new(error, "")), } } + /// <https://w3c.github.io/webdriver/#dfn-release-actions> fn handle_release_actions(&mut self) -> WebDriverResult<WebDriverResponse> { - let input_cancel_list = self.session().unwrap().input_cancel_list.borrow(); - if let Err(error) = self.dispatch_actions(&input_cancel_list) { - return Err(WebDriverError::new(error, "")); - } - + // TODO: The previous implementation of this function was different from the spec. + // Need to re-implement this to match the spec. let session = self.session()?; + // Step 1. If session's current browsing context is no longer open, + // return error with error code no such window. + self.verify_browsing_context_is_open(session.browsing_context_id)?; session.input_state_table.borrow_mut().clear(); Ok(WebDriverResponse::Void) @@ -1614,17 +1683,28 @@ impl Handler { keys: &SendKeysParameters, ) -> WebDriverResult<WebDriverResponse> { let browsing_context_id = self.session()?.browsing_context_id; - + // Step 3. If session's current browsing context is no longer open, + // return error with error code no such window. + self.verify_browsing_context_is_open(browsing_context_id)?; let (sender, receiver) = ipc::channel().unwrap(); - let cmd = WebDriverScriptCommand::FocusElement(element.to_string(), sender); + let cmd = WebDriverScriptCommand::WillSendKeys( + element.to_string(), + keys.text.to_string(), + self.session()?.strict_file_interactability, + sender, + ); let cmd_msg = WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd); self.constellation_chan .send(EmbedderToConstellationMessage::WebDriverCommand(cmd_msg)) .unwrap(); // TODO: distinguish the not found and not focusable cases - wait_for_script_response(receiver)?.map_err(|error| WebDriverError::new(error, ""))?; + // File input and non-typeable form control should have + // been handled in `webdriver_handler.rs`. + if !wait_for_script_response(receiver)?.map_err(|error| WebDriverError::new(error, ""))? { + return Ok(WebDriverResponse::Void); + } let input_events = send_keys(&keys.text); @@ -1655,7 +1735,7 @@ impl Handler { // Step 8.1 self.session_mut()?.input_state_table.borrow_mut().insert( id.clone(), - InputSourceState::Pointer(PointerInputState::new(&PointerType::Mouse)), + InputSourceState::Pointer(PointerInputState::new(PointerType::Mouse)), ); // Step 8.7. Construct a pointer move action. @@ -1702,7 +1782,11 @@ impl Handler { }, }; - let _ = self.dispatch_actions(&[action_sequence]); + let actions_by_tick = self.actions_by_tick_from_sequence(vec![action_sequence]); + + if let Err(e) = self.dispatch_actions(actions_by_tick) { + log::error!("handle_element_click: dispatch_actions failed: {:?}", e); + } // Step 8.17 Remove an input source with input state and input id. self.session_mut()? @@ -1721,6 +1805,9 @@ impl Handler { } fn take_screenshot(&self, rect: Option<Rect<f32, CSSPixel>>) -> WebDriverResult<String> { + // Step 1. If session's current top-level browsing context is no longer open, + // return error with error code no such window. + self.verify_top_level_browsing_context_is_open(self.session()?.webview_id)?; let mut img = None; let interval = 1000; @@ -1868,6 +1955,46 @@ impl Handler { serde_json::to_value(map)?, ))) } + + fn verify_top_level_browsing_context_is_open( + &self, + webview_id: WebViewId, + ) -> Result<(), WebDriverError> { + let (sender, receiver) = ipc::channel().unwrap(); + self.constellation_chan + .send(EmbedderToConstellationMessage::WebDriverCommand( + WebDriverCommandMsg::IsWebViewOpen(webview_id, sender), + )) + .unwrap(); + if !receiver.recv().unwrap_or(false) { + Err(WebDriverError::new( + ErrorStatus::NoSuchWindow, + "No such window", + )) + } else { + Ok(()) + } + } + + fn verify_browsing_context_is_open( + &self, + browsing_context_id: BrowsingContextId, + ) -> Result<(), WebDriverError> { + let (sender, receiver) = ipc::channel().unwrap(); + self.constellation_chan + .send(EmbedderToConstellationMessage::WebDriverCommand( + WebDriverCommandMsg::IsBrowsingContextOpen(browsing_context_id, sender), + )) + .unwrap(); + if !receiver.recv().unwrap_or(false) { + Err(WebDriverError::new( + ErrorStatus::NoSuchWindow, + "No such window", + )) + } else { + Ok(()) + } + } } impl WebDriverHandler<ServoExtensionRoute> for Handler { @@ -1921,6 +2048,7 @@ impl WebDriverHandler<ServoExtensionRoute> for Handler { WebDriverCommand::FindElementElements(ref element, ref parameters) => { self.handle_find_elements_from_element(element, parameters) }, + WebDriverCommand::GetShadowRoot(element) => self.handle_get_shadow_root(element), WebDriverCommand::GetNamedCookie(name) => self.handle_get_cookie(name), WebDriverCommand::GetCookies => self.handle_get_cookies(), WebDriverCommand::GetActiveElement => self.handle_active_element(), @@ -1940,7 +2068,9 @@ impl WebDriverHandler<ServoExtensionRoute> for Handler { self.handle_element_css(element, name) }, WebDriverCommand::GetPageSource => self.handle_get_page_source(), - WebDriverCommand::PerformActions(ref x) => self.handle_perform_actions(x), + WebDriverCommand::PerformActions(actions_parameters) => { + self.handle_perform_actions(actions_parameters) + }, WebDriverCommand::ReleaseActions => self.handle_release_actions(), WebDriverCommand::ExecuteScript(ref x) => self.handle_execute_script(x), WebDriverCommand::ExecuteAsyncScript(ref x) => self.handle_execute_async_script(x), |