From 4cbadde144b1384146cbc76b9ea8c17f463e185a Mon Sep 17 00:00:00 2001 From: Andrei Volykhin Date: Wed, 2 Jul 2025 12:29:47 +0300 Subject: [PATCH] htmlimageelement: Reject decode promises on the current request mutation (#37828) Follow the HTML specification and reject the pending image decode promises on any current request mutation. https://html.spec.whatwg.org/multipage/#updating-the-image-data (step 18) https://html.spec.whatwg.org/multipage/#dom-img-decode (step 3) Fulfill and reject image decode promises by queueing a global tasks on the DOM manipulation task source. Testing: Improvements in the following tests - html/semantics/embedded-content/the-img-element/decode/* Signed-off-by: Andrei Volykhin --- components/script/dom/bindings/error.rs | 1 + components/script/dom/htmlimageelement.rs | 73 ++++++++++++++----- components/script_bindings/error.rs | 3 + .../decode/image-decode-path-changes.html.ini | 15 ---- .../image-decode-with-quick-attach.html.ini | 14 +--- 5 files changed, 58 insertions(+), 48 deletions(-) delete mode 100644 tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html.ini diff --git a/components/script/dom/bindings/error.rs b/components/script/dom/bindings/error.rs index 3777a8ba861..3cd70bdf0f8 100644 --- a/components/script/dom/bindings/error.rs +++ b/components/script/dom/bindings/error.rs @@ -97,6 +97,7 @@ pub(crate) fn throw_dom_exception( Error::NotReadable => DOMErrorName::NotReadableError, Error::Operation => DOMErrorName::OperationError, Error::NotAllowed => DOMErrorName::NotAllowedError, + Error::Encoding => DOMErrorName::EncodingError, Error::Type(message) => unsafe { assert!(!JS_IsExceptionPending(*cx)); throw_type_error(*cx, &message); diff --git a/components/script/dom/htmlimageelement.rs b/components/script/dom/htmlimageelement.rs index 9cee1f202c4..a9190096c6f 100644 --- a/components/script/dom/htmlimageelement.rs +++ b/components/script/dom/htmlimageelement.rs @@ -43,8 +43,6 @@ use style::values::specified::source_size_list::SourceSizeList; use style_traits::ParsingMode; use url::Url; -use super::domexception::DOMErrorName; -use super::types::DOMException; use crate::document_loader::{LoadBlocker, LoadType}; use crate::dom::activation::Activatable; use crate::dom::attr::Attr; @@ -57,7 +55,7 @@ use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMeth use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::inheritance::Castable; -use crate::dom::bindings::refcounted::Trusted; +use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::str::{DOMString, USVString}; @@ -486,7 +484,7 @@ impl HTMLImageElement { LoadBlocker::terminate(&self.current_request.borrow().blocker, can_gc); // Mark the node dirty self.upcast::().dirty(NodeDamage::Other); - self.resolve_image_decode_promises(can_gc); + self.resolve_image_decode_promises(); } /// Step 24 of @@ -597,9 +595,9 @@ impl HTMLImageElement { request.metadata = None; if matches!(state, State::Broken) { - self.reject_image_decode_promises(can_gc); + self.reject_image_decode_promises(); } else if matches!(state, State::CompletelyAvailable) { - self.resolve_image_decode_promises(can_gc); + self.resolve_image_decode_promises(); } } @@ -894,6 +892,7 @@ impl HTMLImageElement { // Step 17 current_request.current_pixel_density = Some(selected_pixel_density); self.init_image_request(&mut current_request, url, src, can_gc); + self.reject_image_decode_promises(); }, (_, _) => { // step 17 @@ -1186,10 +1185,7 @@ impl HTMLImageElement { if !document.is_fully_active() || matches!(self.current_request.borrow().state, State::Broken) { - promise.reject_native( - &DOMException::new(&document.global(), DOMErrorName::EncodingError, can_gc), - can_gc, - ); + promise.reject_error(Error::Encoding, can_gc); } else if matches!( self.current_request.borrow().state, State::CompletelyAvailable @@ -1203,22 +1199,59 @@ impl HTMLImageElement { } } - fn resolve_image_decode_promises(&self, can_gc: CanGc) { - for promise in self.image_decode_promises.borrow().iter() { - promise.resolve_native(&(), can_gc); + /// + fn resolve_image_decode_promises(&self) { + if self.image_decode_promises.borrow().is_empty() { + return; } + + // Step 3. If the decoding process completes successfully, then queue a + // global task on the DOM manipulation task source with global to + // resolve promise with undefined. + let trusted_image_decode_promises: Vec = self + .image_decode_promises + .borrow() + .iter() + .map(|promise| TrustedPromise::new(promise.clone())) + .collect(); + self.image_decode_promises.borrow_mut().clear(); + + self.owner_global() + .task_manager() + .dom_manipulation_task_source() + .queue(task!(fulfill_image_decode_promises: move || { + for trusted_promise in trusted_image_decode_promises { + trusted_promise.root().resolve_native(&(), CanGc::note()); + } + })); } - fn reject_image_decode_promises(&self, can_gc: CanGc) { - let document = self.owner_document(); - for promise in self.image_decode_promises.borrow().iter() { - promise.reject_native( - &DOMException::new(&document.global(), DOMErrorName::EncodingError, can_gc), - can_gc, - ); + /// + fn reject_image_decode_promises(&self) { + if self.image_decode_promises.borrow().is_empty() { + return; } + + // Step 3. Queue a global task on the DOM manipulation task source with + // global to reject promise with an "EncodingError" DOMException. + let trusted_image_decode_promises: Vec = self + .image_decode_promises + .borrow() + .iter() + .map(|promise| TrustedPromise::new(promise.clone())) + .collect(); + self.image_decode_promises.borrow_mut().clear(); + + self.owner_global() + .task_manager() + .dom_manipulation_task_source() + .queue(task!(reject_image_decode_promises: move || { + for trusted_promise in trusted_image_decode_promises { + trusted_promise.root().reject_error(Error::Encoding, CanGc::note()); + } + })); } /// Step 15 for diff --git a/components/script_bindings/error.rs b/components/script_bindings/error.rs index 7954b9ff3a6..2d9bee9a3d9 100644 --- a/components/script_bindings/error.rs +++ b/components/script_bindings/error.rs @@ -9,6 +9,7 @@ use crate::codegen::PrototypeList::proto_id_to_name; use crate::script_runtime::JSContext as SafeJSContext; /// DOM exceptions that can be thrown by a native DOM method. +/// #[derive(Clone, Debug, MallocSizeOf)] pub enum Error { /// IndexSizeError DOMException @@ -67,6 +68,8 @@ pub enum Error { Operation, /// NotAllowedError DOMException NotAllowed, + /// EncodingError DOMException + Encoding, /// TypeError JavaScript Error Type(String), diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html.ini deleted file mode 100644 index b0a41a6f415..00000000000 --- a/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html.ini +++ /dev/null @@ -1,15 +0,0 @@ -[image-decode-path-changes.html] - [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following good png decode succeeds.] - expected: FAIL - - [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode; following good svg decode succeeds.] - expected: FAIL - - [HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode; following good decode succeeds.] - expected: FAIL - - [HTMLImageElement.prototype.decode(), src/srcset mutation tests. src changes fail decode.] - expected: FAIL - - [HTMLImageElement.prototype.decode(), src/srcset mutation tests. srcset changes fail decode.] - expected: FAIL diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach.html.ini index 4cf66fd35ac..11ee505bb02 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach.html.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/decode/image-decode-with-quick-attach.html.ini @@ -1,15 +1,3 @@ [image-decode-with-quick-attach.html] - [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in empty picture not cached] - expected: FAIL - - [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: srcset in empty picture] - expected: FAIL - - [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in picture with source not cached] - expected: FAIL - - [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in picture with source cached] - expected: FAIL - - [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: srcset in picture with source] + [HTMLImageElement.prototype.decode(), attach to DOM before promise resolves.: src in empty picture cached] expected: FAIL