From 5cc371d54c8addd7fd49cd4b51e00c4ad03be6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Mon, 7 Apr 2025 10:46:22 +0000 Subject: [PATCH 01/83] LibWeb: Fix typos - act II --- Libraries/LibWeb/CSS/CSSAnimation.cpp | 2 +- .../EventTiming/PerformanceEventTiming.cpp | 14 ++++++------- Libraries/LibWeb/Geometry/DOMQuad.cpp | 10 +++++----- Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp | 2 +- .../LibWeb/HTML/CanvasRenderingContext2D.cpp | 4 ++-- Libraries/LibWeb/HTML/HTMLElement.cpp | 2 +- Libraries/LibWeb/HTML/HTMLInputElement.cpp | 8 ++++---- Libraries/LibWeb/HTML/HTMLInputElement.h | 2 +- Libraries/LibWeb/HTML/HTMLMediaElement.cpp | 8 ++++---- Libraries/LibWeb/HTML/NavigateEvent.cpp | 4 ++-- Libraries/LibWeb/HTML/NavigatorDeviceMemory.h | 2 +- Libraries/LibWeb/HTML/StructuredSerialize.cpp | 2 +- .../LibWeb/Layout/GridFormattingContext.cpp | 20 +++++++++---------- .../LibWeb/Layout/TableFormattingContext.cpp | 8 ++++---- .../MediaCapabilities.cpp | 2 +- Libraries/LibWeb/Painting/BorderPainting.cpp | 2 +- .../LibWeb/Painting/RadioButtonPaintable.cpp | 2 +- Libraries/LibWeb/SVG/SVGElement.cpp | 2 +- Libraries/LibWeb/SVG/SVGUseElement.cpp | 8 ++++---- Libraries/LibWeb/SVG/SVGUseElement.h | 2 +- Libraries/LibWeb/StorageAPI/StorageEndpoint.h | 2 +- Libraries/LibWeb/UIEvents/KeyboardEvent.cpp | 2 +- .../LibWeb/WebAudio/ChannelSplitterNode.cpp | 2 +- .../WebGL/WebGLRenderingContextBase.idl | 2 +- Libraries/LibWeb/WebSockets/WebSocket.cpp | 6 +++--- Libraries/LibWeb/Worker/WebWorkerClient.cpp | 2 +- Libraries/LibWebView/Application.cpp | 4 ++-- Libraries/LibWebView/ViewImplementation.cpp | 6 +++--- Libraries/LibWebView/ViewImplementation.h | 2 +- Libraries/LibWebView/WebContentClient.cpp | 4 ++-- 30 files changed, 69 insertions(+), 69 deletions(-) diff --git a/Libraries/LibWeb/CSS/CSSAnimation.cpp b/Libraries/LibWeb/CSS/CSSAnimation.cpp index 3a431da28b5..19a66c2e86d 100644 --- a/Libraries/LibWeb/CSS/CSSAnimation.cpp +++ b/Libraries/LibWeb/CSS/CSSAnimation.cpp @@ -24,7 +24,7 @@ Optional CSSAnimation::class_specific_composite_order(GC::Ref(*other_animation) }; - // The existance of an owning element determines the animation class, so both animations should have their owning + // The existence of an owning element determines the animation class, so both animations should have their owning // element in the same state VERIFY(!owning_element() == !other->owning_element()); diff --git a/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp b/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp index 95e1f25f50b..cd0d3567d3e 100644 --- a/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp +++ b/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp @@ -36,13 +36,13 @@ FlyString const& PerformanceEventTiming::entry_type() const HighResolutionTime::DOMHighResTimeStamp PerformanceEventTiming::processing_end() const { - dbgln("FIXME: Implement PeformanceEventTiming processing_end()"); + dbgln("FIXME: Implement PerformanceEventTiming processing_end()"); return 0; } HighResolutionTime::DOMHighResTimeStamp PerformanceEventTiming::processing_start() const { - dbgln("FIXME: Implement PeformanceEventTiming processing_start()"); + dbgln("FIXME: Implement PerformanceEventTiming processing_start()"); return 0; } @@ -53,20 +53,20 @@ bool PerformanceEventTiming::cancelable() const JS::ThrowCompletionOr> PerformanceEventTiming::target() { - dbgln("FIXME: Implement PerformanceEventTiming::PeformanceEventTiming target()"); + dbgln("FIXME: Implement PerformanceEventTiming::PerformanceEventTiming target()"); return nullptr; } unsigned long long PerformanceEventTiming::interaction_id() { - dbgln("FIXME: Implement PeformanceEventTiming interaction_id()"); + dbgln("FIXME: Implement PerformanceEventTiming interaction_id()"); return 0; } // https://www.w3.org/TR/event-timing/#sec-should-add-performanceeventtiming PerformanceTimeline::ShouldAddEntry PerformanceEventTiming::should_add_performance_event_timing() const { - dbgln("FIXME: Implement PeformanceEventTiming should_add_performance_event_timing()"); + dbgln("FIXME: Implement PerformanceEventTiming should_add_performance_event_timing()"); // 1. If entry’s entryType attribute value equals to "first-input", return true. if (entry_type() == "first-input") return PerformanceTimeline::ShouldAddEntry::Yes; @@ -89,7 +89,7 @@ PerformanceTimeline::ShouldAddEntry PerformanceEventTiming::should_add_performan // the commented out if statement won't compile PerformanceTimeline::AvailableFromTimeline PerformanceEventTiming::available_from_timeline() { - dbgln("FIXME: Implement PeformanceEventTiming available_from_timeline()"); + dbgln("FIXME: Implement PerformanceEventTiming available_from_timeline()"); // if (entry_type() == "first-input") return PerformanceTimeline::AvailableFromTimeline::Yes; } @@ -98,7 +98,7 @@ PerformanceTimeline::AvailableFromTimeline PerformanceEventTiming::available_fro // FIXME: Same issue as available_from_timeline() above Optional PerformanceEventTiming::max_buffer_size() { - dbgln("FIXME: Implement PeformanceEventTiming max_buffer_size()"); + dbgln("FIXME: Implement PerformanceEventTiming max_buffer_size()"); if (true) //(entry_type() == "first-input") return 1; // else return 150; diff --git a/Libraries/LibWeb/Geometry/DOMQuad.cpp b/Libraries/LibWeb/Geometry/DOMQuad.cpp index e77e0b4bb5a..09363637b01 100644 --- a/Libraries/LibWeb/Geometry/DOMQuad.cpp +++ b/Libraries/LibWeb/Geometry/DOMQuad.cpp @@ -103,17 +103,17 @@ GC::Ref DOMQuad::get_bounds() const } // https://drafts.fxtf.org/geometry/#structured-serialization -WebIDL::ExceptionOr DOMQuad::serialization_steps(HTML::SerializationRecord& serialzied, bool for_storage, HTML::SerializationMemory& memory) +WebIDL::ExceptionOr DOMQuad::serialization_steps(HTML::SerializationRecord& serialized, bool for_storage, HTML::SerializationMemory& memory) { auto& vm = this->vm(); // 1. Set serialized.[[P1]] to the sub-serialization of value’s point 1. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p1, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p1, for_storage, memory))); // 2. Set serialized.[[P2]] to the sub-serialization of value’s point 2. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p2, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p2, for_storage, memory))); // 3. Set serialized.[[P3]] to the sub-serialization of value’s point 3. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p3, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p3, for_storage, memory))); // 4. Set serialized.[[P4]] to the sub-serialization of value’s point 4. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p4, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p4, for_storage, memory))); return {}; } diff --git a/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp b/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp index dac46083c51..22d34afd70a 100644 --- a/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp +++ b/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp @@ -81,7 +81,7 @@ void CanvasPath::bezier_curve_to(double cp1x, double cp1y, double cp2x, double c // 2. Ensure there is a subpath for (cp1x, cp1y) ensure_subpath(cp1x, cp1y); - // 3. Connect the last point in the subpath to the given point (x, y) using a cubic Bézier curve with control poits (cp1x, cp1y) and (cp2x, cp2y). + // 3. Connect the last point in the subpath to the given point (x, y) using a cubic Bézier curve with control points (cp1x, cp1y) and (cp2x, cp2y). // 4. Add the point (x, y) to the subpath. m_path.cubic_bezier_curve_to( Gfx::FloatPoint { cp1x, cp1y }, Gfx::FloatPoint { cp2x, cp2y }, Gfx::FloatPoint { x, y }); diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index f6f3ab882a0..d6fb63f609e 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -263,10 +263,10 @@ Gfx::Path CanvasRenderingContext2D::text_path(StringView text, float x, float y, } // Apply text baseline - // FIXME: Implement CanvasTextBasline::Hanging, Bindings::CanvasTextAlign::Alphabetic and Bindings::CanvasTextAlign::Ideographic for real + // FIXME: Implement CanvasTextBaseline::Hanging, Bindings::CanvasTextAlign::Alphabetic and Bindings::CanvasTextAlign::Ideographic for real // right now they are just handled as textBaseline = top or bottom. // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-textbaseline-hanging - // Default baseline of draw_text is top so do nothing by CanvasTextBaseline::Top and CanvasTextBasline::Hanging + // Default baseline of draw_text is top so do nothing by CanvasTextBaseline::Top and CanvasTextBaseline::Hanging if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Middle) { transform = Gfx::AffineTransform {}.set_translation({ 0, font->pixel_size() / 2 }).multiply(transform); } diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index a19927021f4..1091597f656 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -1121,7 +1121,7 @@ WebIDL::ExceptionOr HTMLElement::check_popover_validity(ExpectedToBeShowin // - ignoreDomState is false and element is not connected; // - element's node document is not fully active; // - ignoreDomState is false and expectedDocument is not null and element's node document is not expectedDocument; - // - element is a dialog element and its is modal flage is set to true; or + // - element is a dialog element and its is modal flag is set to true; or // - FIXME: element's fullscreen flag is set, // then: // 3.1 If throwExceptions is true, then throw an "InvalidStateError" DOMException. diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index bff078f009d..7a63fa03c45 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -145,7 +145,7 @@ void HTMLInputElement::adjust_computed_style(CSS::ComputedProperties& style) style.set_property(CSS::PropertyID::Width, CSS::LengthStyleValue::create(CSS::Length(size(), CSS::Length::Type::Ch))); } - // NOTE: The following line-height check is done for web compatability and usability reasons. + // NOTE: The following line-height check is done for web compatibility and usability reasons. // FIXME: The "normal" line-height value should be calculated but assume 1.0 for now. double normal_line_height = 1.0; double current_line_height = style.line_height().to_double(); @@ -2404,7 +2404,7 @@ WebIDL::ExceptionOr> HTMLInputElement::convert_string_to_date( } // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-date-string -String HTMLInputElement::covert_date_to_string(GC::Ref input) const +String HTMLInputElement::convert_date_to_string(GC::Ref input) const { // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):concept-input-value-date-string if (type_state() == TypeAttributeState::Date) { @@ -2420,7 +2420,7 @@ String HTMLInputElement::covert_date_to_string(GC::Ref input) const return convert_number_to_time_string(input->date_value()); } - dbgln("HTMLInputElement::covert_date_to_string() not implemented for input type {}", type()); + dbgln("HTMLInputElement::convert_date_to_string() not implemented for input type {}", type()); return {}; } @@ -2589,7 +2589,7 @@ WebIDL::ExceptionOr HTMLInputElement::set_value_as_date(Optional> convert_string_to_date(StringView input) const; - String covert_date_to_string(GC::Ref input) const; + String convert_date_to_string(GC::Ref input) const; Optional min() const; Optional max() const; diff --git a/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Libraries/LibWeb/HTML/HTMLMediaElement.cpp index 249e6a0fa4c..5043ccbad74 100644 --- a/Libraries/LibWeb/HTML/HTMLMediaElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLMediaElement.cpp @@ -637,11 +637,11 @@ public: { // 2. ⌛ Process candidate: If candidate does not have a src attribute, or if its src attribute's value is the // empty string, then end the synchronous section, and jump down to the failed with elements step below. - String candiate_src; + String candidate_src; if (auto maybe_src = m_candidate->get_attribute(HTML::AttributeNames::src); maybe_src.has_value()) - candiate_src = *maybe_src; + candidate_src = *maybe_src; - if (candiate_src.is_empty()) { + if (candidate_src.is_empty()) { TRY(failed_with_elements()); return {}; } @@ -649,7 +649,7 @@ public: // 3. ⌛ Let urlString and urlRecord be the resulting URL string and the resulting URL record, respectively, that // would have resulted from parsing the URL specified by candidate's src attribute's value relative to the // candidate's node document when the src attribute was last changed. - auto url_record = m_candidate->document().parse_url(candiate_src); + auto url_record = m_candidate->document().parse_url(candidate_src); // 4. ⌛ If urlString was not obtained successfully, then end the synchronous section, and jump down to the failed // with elements step below. diff --git a/Libraries/LibWeb/HTML/NavigateEvent.cpp b/Libraries/LibWeb/HTML/NavigateEvent.cpp index e14a05b001e..f61e4dc29f6 100644 --- a/Libraries/LibWeb/HTML/NavigateEvent.cpp +++ b/Libraries/LibWeb/HTML/NavigateEvent.cpp @@ -103,7 +103,7 @@ WebIDL::ExceptionOr NavigateEvent::intercept(NavigationInterceptOptions co if (m_focus_reset_behavior.has_value() && *m_focus_reset_behavior != *options.focus_reset) { auto& console = realm.intrinsics().console_object()->console(); console.output_debug_message(JS::Console::LogLevel::Warn, - TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overriden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset))); + TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overridden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset))); } // 2. Set this's focus reset behavior to options["focusReset"]. @@ -118,7 +118,7 @@ WebIDL::ExceptionOr NavigateEvent::intercept(NavigationInterceptOptions co if (m_scroll_behavior.has_value() && *m_scroll_behavior != *options.scroll) { auto& console = realm.intrinsics().console_object()->console(); console.output_debug_message(JS::Console::LogLevel::Warn, - TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overriden (was: {}, now: {})", *m_scroll_behavior, *options.scroll))); + TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overridden (was: {}, now: {})", *m_scroll_behavior, *options.scroll))); } // 2. Set this's scroll behavior to options["scroll"]. diff --git a/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h b/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h index 4a2615e7b93..83eca4e041e 100644 --- a/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h +++ b/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h @@ -18,7 +18,7 @@ public: WebIDL::Double device_memory() const { // The value is calculated by using the actual device memory in MiB then rounding it to the - // nearest number where only the most signicant bit can be set and the rest are zeros + // nearest number where only the most significant bit can be set and the rest are zeros // (nearest power of two). auto memory_in_bytes = Core::System::physical_memory_bytes(); auto memory_in_mib = memory_in_bytes / MiB; diff --git a/Libraries/LibWeb/HTML/StructuredSerialize.cpp b/Libraries/LibWeb/HTML/StructuredSerialize.cpp index cb9cce76474..d9c26e23333 100644 --- a/Libraries/LibWeb/HTML/StructuredSerialize.cpp +++ b/Libraries/LibWeb/HTML/StructuredSerialize.cpp @@ -658,7 +658,7 @@ WebIDL::ExceptionOr serialize_viewed_array_buffer(JS::VM& vm, Vector& auto buffer_serialized = TRY(structured_serialize_internal(vm, buffer, for_storage, memory)); // 4. Assert: bufferSerialized.[[Type]] is "ArrayBuffer", "ResizableArrayBuffer", "SharedArrayBuffer", or "GrowableSharedArrayBuffer". - // NOTE: Object reference + memory check is required when ArrayBuffer is transfered. + // NOTE: Object reference + memory check is required when ArrayBuffer is transferred. auto tag = buffer_serialized[0]; VERIFY(tag == ValueTag::ArrayBuffer || tag == ValueTag::ResizeableArrayBuffer diff --git a/Libraries/LibWeb/Layout/GridFormattingContext.cpp b/Libraries/LibWeb/Layout/GridFormattingContext.cpp index a1b5aabcbad..b3e8caf61e6 100644 --- a/Libraries/LibWeb/Layout/GridFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/GridFormattingContext.cpp @@ -1160,7 +1160,7 @@ void GridFormattingContext::expand_flexible_tracks(GridDimension dimension) auto& tracks_and_gaps = dimension == GridDimension::Column ? m_grid_columns_and_gaps : m_grid_rows_and_gaps; auto& tracks = dimension == GridDimension::Column ? m_grid_columns : m_grid_rows; auto& available_size = dimension == GridDimension::Column ? m_available_space->width : m_available_space->height; - // FIXME: This should idealy take a Span, as that is more idomatic, but Span does not yet support holding references + // FIXME: This should ideally take a Span, as that is more idomatic, but Span does not yet support holding references auto find_the_size_of_an_fr = [&](Vector const& tracks, CSSPixels space_to_fill) -> CSSPixelFraction { // https://www.w3.org/TR/css-grid-2/#algo-find-fr-size auto treat_track_as_inflexiable = MUST(AK::Bitmap::create(tracks.size(), false)); @@ -2398,23 +2398,23 @@ CSSPixels GridFormattingContext::calculate_min_content_contribution(GridItem con return should_treat_height_as_auto(item.box, available_space_for_item); }(); - auto maxium_size = CSSPixels::max(); + auto maximum_size = CSSPixels::max(); if (auto const& css_maximum_size = item.maximum_size(dimension); css_maximum_size.is_length()) { - maxium_size = css_maximum_size.length().to_px(item.box); + maximum_size = css_maximum_size.length().to_px(item.box); } if (should_treat_preferred_size_as_auto) { auto result = item.add_margin_box_sizes(calculate_min_content_size(item, dimension), dimension); - return min(result, maxium_size); + return min(result, maximum_size); } auto preferred_size = item.preferred_size(dimension); if (dimension == GridDimension::Column) { auto width = calculate_inner_width(item.box, m_available_space->width, preferred_size); - return min(item.add_margin_box_sizes(width, dimension), maxium_size); + return min(item.add_margin_box_sizes(width, dimension), maximum_size); } auto height = calculate_inner_height(item.box, *m_available_space, preferred_size); - return min(item.add_margin_box_sizes(height, dimension), maxium_size); + return min(item.add_margin_box_sizes(height, dimension), maximum_size); } CSSPixels GridFormattingContext::calculate_max_content_contribution(GridItem const& item, GridDimension dimension) const @@ -2427,21 +2427,21 @@ CSSPixels GridFormattingContext::calculate_max_content_contribution(GridItem con return should_treat_height_as_auto(item.box, available_space_for_item); }(); - auto maxium_size = CSSPixels::max(); + auto maximum_size = CSSPixels::max(); if (auto const& css_maximum_size = item.maximum_size(dimension); css_maximum_size.is_length()) { - maxium_size = css_maximum_size.length().to_px(item.box); + maximum_size = css_maximum_size.length().to_px(item.box); } auto preferred_size = item.preferred_size(dimension); if (should_treat_preferred_size_as_auto || preferred_size.is_fit_content()) { auto fit_content_size = dimension == GridDimension::Column ? calculate_fit_content_width(item.box, available_space_for_item) : calculate_fit_content_height(item.box, available_space_for_item); auto result = item.add_margin_box_sizes(fit_content_size, dimension); - return min(result, maxium_size); + return min(result, maximum_size); } auto containing_block_size = containing_block_size_for_item(item, dimension); auto result = item.add_margin_box_sizes(preferred_size.to_px(grid_container(), containing_block_size), dimension); - return min(result, maxium_size); + return min(result, maximum_size); } CSSPixels GridFormattingContext::calculate_limited_min_content_contribution(GridItem const& item, GridDimension dimension) const diff --git a/Libraries/LibWeb/Layout/TableFormattingContext.cpp b/Libraries/LibWeb/Layout/TableFormattingContext.cpp index c9e80f33522..c4277db4b66 100644 --- a/Libraries/LibWeb/Layout/TableFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/TableFormattingContext.cpp @@ -295,15 +295,15 @@ void TableFormattingContext::compute_intrinsic_percentage(size_t max_cell_span) // that the cell spans. If this gives a negative result, change it to 0%. // 3. Multiply by the ratio of the column’s non-spanning max-content width to the sum of the non-spanning max-content widths of all // columns spanned by the cell that have an intrinsic percentage width of the column based on cells of span up to N-1 equal to 0%. - CSSPixels ajusted_cell_contribution; + CSSPixels adjusted_cell_contribution; if (width_sum_of_columns_with_zero_intrinsic_percentage != 0) { - ajusted_cell_contribution = cell_contribution.scaled(rows_or_columns[rc_index].max_size / static_cast(width_sum_of_columns_with_zero_intrinsic_percentage)); + adjusted_cell_contribution = cell_contribution.scaled(rows_or_columns[rc_index].max_size / static_cast(width_sum_of_columns_with_zero_intrinsic_percentage)); } else { // However, if this ratio is undefined because the denominator is zero, instead use the 1 divided by the number of columns // spanned by the cell that have an intrinsic percentage width of the column based on cells of span up to N-1 equal to zero. - ajusted_cell_contribution = cell_contribution * 1 / number_of_columns_with_zero_intrinsic_percentage; + adjusted_cell_contribution = cell_contribution * 1 / number_of_columns_with_zero_intrinsic_percentage; } - intrinsic_percentage_contribution_by_index[rc_index] = max(static_cast(ajusted_cell_contribution), intrinsic_percentage_contribution_by_index[rc_index]); + intrinsic_percentage_contribution_by_index[rc_index] = max(static_cast(adjusted_cell_contribution), intrinsic_percentage_contribution_by_index[rc_index]); } } for (size_t rc_index = 0; rc_index < rows_or_columns.size(); ++rc_index) { diff --git a/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp b/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp index b958b635fe3..e229d4e3bee 100644 --- a/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp +++ b/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp @@ -238,7 +238,7 @@ GC::Ref MediaCapabilitiesDecodingInfo::to_object(JS::Realm& realm) MUST(object->create_data_property("supported"_fly_string, JS::BooleanObject::create(realm, supported))); MUST(object->create_data_property("smooth"_fly_string, JS::BooleanObject::create(realm, smooth))); - MUST(object->create_data_property("powerEfficent"_fly_string, JS::BooleanObject::create(realm, power_efficient))); + MUST(object->create_data_property("powerEfficient"_fly_string, JS::BooleanObject::create(realm, power_efficient))); return object; } diff --git a/Libraries/LibWeb/Painting/BorderPainting.cpp b/Libraries/LibWeb/Painting/BorderPainting.cpp index ec7fe534f07..bfb4848891f 100644 --- a/Libraries/LibWeb/Painting/BorderPainting.cpp +++ b/Libraries/LibWeb/Painting/BorderPainting.cpp @@ -213,7 +213,7 @@ void paint_border(DisplayListRecorder& painter, BorderEdge edge, DevicePixelRect VERIFY_NOT_REACHED(); } } - // FIXME: this middle point rule seems not exacly the same as main browsers + // FIXME: this middle point rule seems not exactly the same as main browsers // compute the midpoint based on point whose tangent slope of 1 // https://math.stackexchange.com/questions/3325134/find-the-points-on-the-ellipse-where-the-slope-of-the-tangent-line-is-1 return Gfx::FloatPoint( diff --git a/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp b/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp index 11103e7d7c9..9a6cd727e10 100644 --- a/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp +++ b/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp @@ -39,7 +39,7 @@ void RadioButtonPaintable::paint(PaintContext& context, PaintPhase phase) const return; auto draw_circle = [&](auto const& rect, Color color) { - // Note: Doing this is a bit more forgiving than draw_circle() which will round to the nearset even radius. + // Note: Doing this is a bit more forgiving than draw_circle() which will round to the nearest even radius. // This will fudge it (which works better here). context.display_list_recorder().fill_rect_with_rounded_corners(rect, color, rect.width() / 2); }; diff --git a/Libraries/LibWeb/SVG/SVGElement.cpp b/Libraries/LibWeb/SVG/SVGElement.cpp index 45756e3b639..f7a8d26ad25 100644 --- a/Libraries/LibWeb/SVG/SVGElement.cpp +++ b/Libraries/LibWeb/SVG/SVGElement.cpp @@ -115,7 +115,7 @@ void SVGElement::update_use_elements_that_reference_this() // An unconnected node cannot have valid references. // This also prevents searches for elements that are in the process of being constructed - as clones. || !this->is_connected() - // Each use element already listens for the completely_loaded event and then clones its referece, + // Each use element already listens for the completely_loaded event and then clones its reference, // we do not have to also clone it in the process of initial DOM building. || !document().is_completely_loaded()) { diff --git a/Libraries/LibWeb/SVG/SVGUseElement.cpp b/Libraries/LibWeb/SVG/SVGUseElement.cpp index bcac845abdd..6e55ede6333 100644 --- a/Libraries/LibWeb/SVG/SVGUseElement.cpp +++ b/Libraries/LibWeb/SVG/SVGUseElement.cpp @@ -82,14 +82,14 @@ void SVGUseElement::process_the_url(Optional const& href) if (!m_href.has_value()) return; - if (is_referrenced_element_same_document()) { + if (is_referenced_element_same_document()) { clone_element_tree_as_our_shadow_tree(referenced_element()); } else { fetch_the_document(*m_href); } } -bool SVGUseElement::is_referrenced_element_same_document() const +bool SVGUseElement::is_referenced_element_same_document() const { return m_href->equals(document().url(), URL::ExcludeFragment::Yes); } @@ -121,7 +121,7 @@ void SVGUseElement::svg_element_changed(SVGElement& svg_element) void SVGUseElement::svg_element_removed(SVGElement& svg_element) { - if (!m_href.has_value() || !m_href->fragment().has_value() || !is_referrenced_element_same_document()) { + if (!m_href.has_value() || !m_href->fragment().has_value() || !is_referenced_element_same_document()) { return; } @@ -139,7 +139,7 @@ GC::Ptr SVGUseElement::referenced_element() if (!m_href->fragment().has_value()) return nullptr; - if (is_referrenced_element_same_document()) + if (is_referenced_element_same_document()) return document().get_element_by_id(*m_href->fragment()); if (!m_resource_request) diff --git a/Libraries/LibWeb/SVG/SVGUseElement.h b/Libraries/LibWeb/SVG/SVGUseElement.h index 7a1dea33859..d69a25e3b6e 100644 --- a/Libraries/LibWeb/SVG/SVGUseElement.h +++ b/Libraries/LibWeb/SVG/SVGUseElement.h @@ -57,7 +57,7 @@ private: GC::Ptr referenced_element(); void fetch_the_document(URL::URL const& url); - bool is_referrenced_element_same_document() const; + bool is_referenced_element_same_document() const; void clone_element_tree_as_our_shadow_tree(Element* to_clone); bool is_valid_reference_element(Element const& reference_element) const; diff --git a/Libraries/LibWeb/StorageAPI/StorageEndpoint.h b/Libraries/LibWeb/StorageAPI/StorageEndpoint.h index 450f0c32e2e..3f54c1bd771 100644 --- a/Libraries/LibWeb/StorageAPI/StorageEndpoint.h +++ b/Libraries/LibWeb/StorageAPI/StorageEndpoint.h @@ -25,7 +25,7 @@ struct StorageEndpoint { // https://storage.spec.whatwg.org/#storage-endpoint-types // A storage endpoint also has types, which is a set of storage types. - // NOTE: We do not implement this as a set as it is not neccessary in the current implementation. + // NOTE: We do not implement this as a set as it is not necessary in the current implementation. StorageType type; // https://storage.spec.whatwg.org/#storage-endpoint-quota diff --git a/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp b/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp index fda31a480fb..ab12ba49080 100644 --- a/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp +++ b/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp @@ -307,7 +307,7 @@ static ErrorOr get_event_key(KeyCode platform_key, u32 code_point) // FIXME: 4. Else, if the key event has any modifier keys other than glyph modifier keys, then // FIXME: 1. Set key to the key string that would have been generated by this event if it had been typed with all - // modifer keys removed except for glyph modifier keys. + // modifier keys removed except for glyph modifier keys. // 5. Return key as the key attribute value for this key event. return "Unidentified"_string; diff --git a/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp b/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp index e181a6b44bc..9d654a3722c 100644 --- a/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp +++ b/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp @@ -77,7 +77,7 @@ WebIDL::ExceptionOr ChannelSplitterNode::set_channel_count_mode(Bindings:: WebIDL::ExceptionOr ChannelSplitterNode::set_channel_interpretation(Bindings::ChannelInterpretation channel_interpretation) { // https://webaudio.github.io/web-audio-api/#audionode-channelinterpretation-constraints - // The channel intepretation can not be changed from "discrete" and a InvalidStateError exception MUST be thrown for any attempt to change the value. + // The channel interpretation can not be changed from "discrete" and a InvalidStateError exception MUST be thrown for any attempt to change the value. if (channel_interpretation != Bindings::ChannelInterpretation::Discrete) return WebIDL::InvalidStateError::create(realm(), "Channel interpretation must be 'discrete'"_string); diff --git a/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl b/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl index 96870119577..82636b16a1e 100644 --- a/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl +++ b/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl @@ -333,7 +333,7 @@ interface mixin WebGLRenderingContextBase { const GLenum SAMPLE_COVERAGE_VALUE = 0x80AA; const GLenum SAMPLE_COVERAGE_INVERT = 0x80AB; - // GetTexureParameter + // GetTextureParameter const GLenum COMPRESSED_TEXTURE_FORMATS = 0x86A3; // HintMode diff --git a/Libraries/LibWeb/WebSockets/WebSocket.cpp b/Libraries/LibWeb/WebSockets/WebSocket.cpp index 35258f9bb3c..d4868bbb860 100644 --- a/Libraries/LibWeb/WebSockets/WebSocket.cpp +++ b/Libraries/LibWeb/WebSockets/WebSocket.cpp @@ -193,9 +193,9 @@ ErrorOr WebSocket::establish_web_socket_connection(URL::URL const& url_rec auto& window_or_worker = as(client.global_object()); auto origin_string = window_or_worker.origin().to_byte_string(); - Vector protcol_byte_strings; + Vector protocol_byte_strings; for (auto const& protocol : protocols) - TRY(protcol_byte_strings.try_append(protocol.to_byte_string())); + TRY(protocol_byte_strings.try_append(protocol.to_byte_string())); HTTP::HeaderMap additional_headers; @@ -213,7 +213,7 @@ ErrorOr WebSocket::establish_web_socket_connection(URL::URL const& url_rec additional_headers.set("Cookie", cookies.to_byte_string()); } - m_websocket = ResourceLoader::the().request_client().websocket_connect(url_record, origin_string, protcol_byte_strings, {}, additional_headers); + m_websocket = ResourceLoader::the().request_client().websocket_connect(url_record, origin_string, protocol_byte_strings, {}, additional_headers); m_websocket->on_open = [weak_this = make_weak_ptr()] { if (!weak_this) diff --git a/Libraries/LibWeb/Worker/WebWorkerClient.cpp b/Libraries/LibWeb/Worker/WebWorkerClient.cpp index c4a63fae79a..1778595ff39 100644 --- a/Libraries/LibWeb/Worker/WebWorkerClient.cpp +++ b/Libraries/LibWeb/Worker/WebWorkerClient.cpp @@ -11,7 +11,7 @@ namespace Web::HTML { void WebWorkerClient::die() { - // FIXME: Notify WorkerAgent that the worker is ded + // FIXME: Notify WorkerAgent that the worker is dead } void WebWorkerClient::did_close_worker() diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index 8faee7a4ce3..3c5a448afc8 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -512,8 +512,8 @@ static void edit_dom_node(DevTools::TabDescription const& description, Applicati return; } - view->on_finshed_editing_dom_node = [&view = *view, on_complete = move(on_complete)](auto node_id) { - view.on_finshed_editing_dom_node = nullptr; + view->on_finished_editing_dom_node = [&view = *view, on_complete = move(on_complete)](auto node_id) { + view.on_finished_editing_dom_node = nullptr; if (node_id.has_value()) on_complete(*node_id); diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index 5d823ff0591..6c83ad5ac3d 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -688,7 +688,7 @@ NonnullRefPtr> ViewImplementation::take_screenshot(Sc auto promise = Core::Promise::construct(); if (m_pending_screenshot) { - // For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need + // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need // to allow spamming screenshot requests for some reason. promise->reject(Error::from_string_literal("A screenshot request is already in progress")); return promise; @@ -720,7 +720,7 @@ NonnullRefPtr> ViewImplementation::take_dom_node_scre auto promise = Core::Promise::construct(); if (m_pending_screenshot) { - // For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need + // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need // to allow spamming screenshot requests for some reason. promise->reject(Error::from_string_literal("A screenshot request is already in progress")); return promise; @@ -749,7 +749,7 @@ NonnullRefPtr> ViewImplementation::request_internal_page_i auto promise = Core::Promise::construct(); if (m_pending_info_request) { - // For simplicitly, only allow one info request at a time for now. + // For simplicity, only allow one info request at a time for now. promise->reject(Error::from_string_literal("A page info request is already in progress")); return promise; } diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 5a60dcefdfd..d43bc370c47 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -201,7 +201,7 @@ public: Function on_received_accessibility_tree; Function on_received_hovered_node_id; Function on_dom_mutation_received; - Function const& node_id)> on_finshed_editing_dom_node; + Function const& node_id)> on_finished_editing_dom_node; Function on_received_dom_node_html; Function)> on_received_style_sheet_list; Function on_received_style_sheet_source; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index ede5a7fadc2..ebffe484135 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -341,8 +341,8 @@ void WebContentClient::did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID no void WebContentClient::did_finish_editing_dom_node(u64 page_id, Optional node_id) { if (auto view = view_for_page_id(page_id); view.has_value()) { - if (view->on_finshed_editing_dom_node) - view->on_finshed_editing_dom_node(node_id); + if (view->on_finished_editing_dom_node) + view->on_finished_editing_dom_node(node_id); } } From da1ff1ba4064d0424b262d6590e39814fe2ead98 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 8 Apr 2025 13:18:56 +0100 Subject: [PATCH 02/83] LibWeb/CSS: Store CSSStyleSheet location as a URL We already have a URL when we construct these, and we want a URL later, so avoid serializing and re-parsing it. --- Libraries/LibWeb/CSS/CSSStyleSheet.cpp | 6 +++--- Libraries/LibWeb/CSS/StyleSheet.cpp | 7 +++++++ Libraries/LibWeb/CSS/StyleSheet.h | 9 +++++---- Libraries/LibWeb/CSS/StyleSheetList.cpp | 2 +- Libraries/LibWeb/DOM/Document.cpp | 2 +- Libraries/LibWeb/HTML/HTMLLinkElement.cpp | 2 +- Services/WebContent/PageClient.cpp | 4 ++-- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp index 0500e1989a0..39cb95964c6 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp @@ -37,13 +37,13 @@ WebIDL::ExceptionOr> CSSStyleSheet::construct_impl(JS::Re // 2. Set sheet’s location to the base URL of the associated Document for the current principal global object. auto associated_document = as(HTML::current_principal_global_object()).document(); - sheet->set_location(associated_document->base_url().to_string()); + sheet->set_location(associated_document->base_url()); // 3. Set sheet’s stylesheet base URL to the baseURL attribute value from options. if (options.has_value() && options->base_url.has_value()) { Optional sheet_location_url; if (sheet->location().has_value()) - sheet_location_url = URL::Parser::basic_parse(sheet->location().release_value()); + sheet_location_url = sheet->location().release_value(); // AD-HOC: This isn't explicitly mentioned in the specification, but multiple modern browsers do this. Optional url = sheet->location().has_value() ? sheet_location_url->complete_url(options->base_url.value()) : URL::Parser::basic_parse(options->base_url.value()); @@ -100,7 +100,7 @@ CSSStyleSheet::CSSStyleSheet(JS::Realm& realm, CSSRuleList& rules, MediaList& me , m_rules(&rules) { if (location.has_value()) - set_location(location->to_string()); + set_location(move(location)); for (auto& rule : *m_rules) rule->set_parent_style_sheet(this); diff --git a/Libraries/LibWeb/CSS/StyleSheet.cpp b/Libraries/LibWeb/CSS/StyleSheet.cpp index 2d908e00c15..f1387c98b47 100644 --- a/Libraries/LibWeb/CSS/StyleSheet.cpp +++ b/Libraries/LibWeb/CSS/StyleSheet.cpp @@ -27,6 +27,13 @@ void StyleSheet::visit_edges(Cell::Visitor& visitor) visitor.visit(m_media); } +Optional StyleSheet::href() const +{ + if (m_location.has_value()) + return m_location->to_string(); + return {}; +} + void StyleSheet::set_owner_node(DOM::Element* element) { m_owner_node = element; diff --git a/Libraries/LibWeb/CSS/StyleSheet.h b/Libraries/LibWeb/CSS/StyleSheet.h index 3e73f0e3f75..8f03bcdd7e0 100644 --- a/Libraries/LibWeb/CSS/StyleSheet.h +++ b/Libraries/LibWeb/CSS/StyleSheet.h @@ -13,6 +13,7 @@ namespace Web::CSS { +// https://drafts.csswg.org/cssom-1/#the-stylesheet-interface class StyleSheet : public Bindings::PlatformObject { WEB_PLATFORM_OBJECT(StyleSheet, Bindings::PlatformObject); @@ -24,10 +25,10 @@ public: DOM::Element* owner_node() { return m_owner_node; } void set_owner_node(DOM::Element*); - Optional href() const { return m_location; } + Optional href() const; - Optional location() const { return m_location; } - void set_location(Optional location) { m_location = move(location); } + Optional location() const { return m_location; } + void set_location(Optional location) { m_location = move(location); } String title() const { return m_title; } Optional title_for_bindings() const; @@ -67,7 +68,7 @@ private: GC::Ptr m_owner_node; GC::Ptr m_parent_style_sheet; - Optional m_location; + Optional m_location; String m_title; String m_type_string; diff --git a/Libraries/LibWeb/CSS/StyleSheetList.cpp b/Libraries/LibWeb/CSS/StyleSheetList.cpp index e7e204f8599..18c49b2db11 100644 --- a/Libraries/LibWeb/CSS/StyleSheetList.cpp +++ b/Libraries/LibWeb/CSS/StyleSheetList.cpp @@ -60,7 +60,7 @@ void StyleSheetList::add_a_css_style_sheet(CSS::CSSStyleSheet& sheet) } // https://www.w3.org/TR/cssom/#create-a-css-style-sheet -void StyleSheetList::create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet& sheet) +void StyleSheetList::create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet& sheet) { // 1. Create a new CSS style sheet object and set its properties as specified. // FIXME: We receive `sheet` from the caller already. This is weird. diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index e8a3d0b437e..759c4dbd891 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -5879,7 +5879,7 @@ void Document::for_each_active_css_style_sheet(Function find_style_sheet_with_url(String const& url, CSS::CSSStyleSheet& style_sheet) { - if (style_sheet.location() == url) + if (style_sheet.href() == url) return style_sheet; for (auto& import_rule : style_sheet.import_rules()) { diff --git a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp index d5242517c4c..a37b421c82c 100644 --- a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp @@ -492,7 +492,7 @@ void HTMLLinkElement::process_stylesheet_resource(bool success, Fetch::Infrastru if (m_loaded_style_sheet) { Optional location; if (!response.url_list().is_empty()) - location = response.url_list().first().to_string(); + location = response.url_list().first(); document_or_shadow_root_style_sheets().create_a_css_style_sheet( "text/css"_string, diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index 57b57b7ce55..1749b33db44 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -834,8 +834,8 @@ static void gather_style_sheets(Vector& results, } if (valid) { - if (auto location = sheet.location(); location.has_value()) - identifier.url = location.release_value(); + if (auto sheet_url = sheet.href(); sheet_url.has_value()) + identifier.url = sheet_url.release_value(); identifier.rule_count = sheet.rules().length(); results.append(move(identifier)); From c82f4b46a2aefd1fc90168c38bc913850f74ee29 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 8 Apr 2025 13:35:26 +0100 Subject: [PATCH 03/83] LibWeb/CSS: Qualify uses of LibURL To prepare for introducing a CSS::URL type, we need to qualify any use of LibURL as `::URL::foo` instead of `URL::foo` so the compiler doesn't get confused. Many of these uses will be replaced, but I don't want to mix this in with what will likely already be a large change. --- Libraries/LibWeb/CSS/CSSImportRule.cpp | 4 ++-- Libraries/LibWeb/CSS/CSSImportRule.h | 8 +++---- Libraries/LibWeb/CSS/CSSStyleSheet.cpp | 8 +++---- Libraries/LibWeb/CSS/CSSStyleSheet.h | 11 +++++----- Libraries/LibWeb/CSS/ComputedValues.h | 22 +++++++++---------- Libraries/LibWeb/CSS/Fetch.cpp | 2 +- Libraries/LibWeb/CSS/ParsedFontFace.cpp | 2 +- Libraries/LibWeb/CSS/ParsedFontFace.h | 2 +- Libraries/LibWeb/CSS/Parser/Helpers.cpp | 2 +- Libraries/LibWeb/CSS/Parser/Parser.cpp | 16 +++++++------- Libraries/LibWeb/CSS/Parser/Parser.h | 20 ++++++++--------- Libraries/LibWeb/CSS/Parser/RuleParsing.cpp | 2 +- Libraries/LibWeb/CSS/Parser/ValueParsing.cpp | 6 ++--- Libraries/LibWeb/CSS/StyleComputer.cpp | 8 +++---- Libraries/LibWeb/CSS/StyleComputer.h | 4 ++-- Libraries/LibWeb/CSS/StyleSheet.h | 6 ++--- Libraries/LibWeb/CSS/StyleSheetList.cpp | 2 +- Libraries/LibWeb/CSS/StyleSheetList.h | 2 +- .../CSS/StyleValues/FontSourceStyleValue.cpp | 6 ++--- .../CSS/StyleValues/FontSourceStyleValue.h | 2 +- .../CSS/StyleValues/ImageStyleValue.cpp | 2 +- .../LibWeb/CSS/StyleValues/ImageStyleValue.h | 6 ++--- .../LibWeb/CSS/StyleValues/URLStyleValue.h | 8 +++---- Libraries/LibWeb/HTML/HTMLLinkElement.cpp | 2 +- 24 files changed, 77 insertions(+), 76 deletions(-) diff --git a/Libraries/LibWeb/CSS/CSSImportRule.cpp b/Libraries/LibWeb/CSS/CSSImportRule.cpp index 49f61e89b56..7e20b09ea3c 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.cpp +++ b/Libraries/LibWeb/CSS/CSSImportRule.cpp @@ -23,13 +23,13 @@ namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSImportRule); -GC::Ref CSSImportRule::create(URL::URL url, DOM::Document& document, RefPtr supports, Vector> media_query_list) +GC::Ref CSSImportRule::create(::URL::URL url, DOM::Document& document, RefPtr supports, Vector> media_query_list) { auto& realm = document.realm(); return realm.create(move(url), document, supports, move(media_query_list)); } -CSSImportRule::CSSImportRule(URL::URL url, DOM::Document& document, RefPtr supports, Vector> media_query_list) +CSSImportRule::CSSImportRule(::URL::URL url, DOM::Document& document, RefPtr supports, Vector> media_query_list) : CSSRule(document.realm(), Type::Import) , m_url(move(url)) , m_document(document) diff --git a/Libraries/LibWeb/CSS/CSSImportRule.h b/Libraries/LibWeb/CSS/CSSImportRule.h index a3712ed3bfa..4426b156787 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.h +++ b/Libraries/LibWeb/CSS/CSSImportRule.h @@ -21,11 +21,11 @@ class CSSImportRule final GC_DECLARE_ALLOCATOR(CSSImportRule); public: - [[nodiscard]] static GC::Ref create(URL::URL, DOM::Document&, RefPtr, Vector>); + [[nodiscard]] static GC::Ref create(::URL::URL, DOM::Document&, RefPtr, Vector>); virtual ~CSSImportRule() = default; - URL::URL const& url() const { return m_url; } + ::URL::URL const& url() const { return m_url; } // FIXME: This should return only the specified part of the url. eg, "stuff/foo.css", not "https://example.com/stuff/foo.css". String href() const { return m_url.to_string(); } @@ -37,7 +37,7 @@ public: Optional supports_text() const; private: - CSSImportRule(URL::URL, DOM::Document&, RefPtr, Vector>); + CSSImportRule(::URL::URL, DOM::Document&, RefPtr, Vector>); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -49,7 +49,7 @@ private: void fetch(); void set_style_sheet(GC::Ref); - URL::URL m_url; + ::URL::URL m_url; GC::Ptr m_document; RefPtr m_supports; Vector> m_media_query_list; diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp index 39cb95964c6..f929a1e7131 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp @@ -24,7 +24,7 @@ namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSStyleSheet); -GC::Ref CSSStyleSheet::create(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional location) +GC::Ref CSSStyleSheet::create(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional<::URL::URL> location) { return realm.create(realm, rules, media, move(location)); } @@ -41,12 +41,12 @@ WebIDL::ExceptionOr> CSSStyleSheet::construct_impl(JS::Re // 3. Set sheet’s stylesheet base URL to the baseURL attribute value from options. if (options.has_value() && options->base_url.has_value()) { - Optional sheet_location_url; + Optional<::URL::URL> sheet_location_url; if (sheet->location().has_value()) sheet_location_url = sheet->location().release_value(); // AD-HOC: This isn't explicitly mentioned in the specification, but multiple modern browsers do this. - Optional url = sheet->location().has_value() ? sheet_location_url->complete_url(options->base_url.value()) : URL::Parser::basic_parse(options->base_url.value()); + Optional<::URL::URL> url = sheet->location().has_value() ? sheet_location_url->complete_url(options->base_url.value()) : ::URL::Parser::basic_parse(options->base_url.value()); if (!url.has_value()) return WebIDL::NotAllowedError::create(realm, "Constructed style sheets must have a valid base URL"_string); @@ -95,7 +95,7 @@ WebIDL::ExceptionOr> CSSStyleSheet::construct_impl(JS::Re return sheet; } -CSSStyleSheet::CSSStyleSheet(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional location) +CSSStyleSheet::CSSStyleSheet(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional<::URL::URL> location) : StyleSheet(realm, media) , m_rules(&rules) { diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.h b/Libraries/LibWeb/CSS/CSSStyleSheet.h index b3c93b102b6..aba78f28c1c 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.h +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.h @@ -27,12 +27,13 @@ struct CSSStyleSheetInit { bool disabled { false }; }; +// https://drafts.csswg.org/cssom-1/#cssstylesheet class CSSStyleSheet final : public StyleSheet { WEB_PLATFORM_OBJECT(CSSStyleSheet, StyleSheet); GC_DECLARE_ALLOCATOR(CSSStyleSheet); public: - [[nodiscard]] static GC::Ref create(JS::Realm&, CSSRuleList&, MediaList&, Optional location); + [[nodiscard]] static GC::Ref create(JS::Realm&, CSSRuleList&, MediaList&, Optional<::URL::URL> location); static WebIDL::ExceptionOr> construct_impl(JS::Realm&, Optional const& options = {}); virtual ~CSSStyleSheet() override = default; @@ -74,8 +75,8 @@ public: Vector> const& import_rules() const { return m_import_rules; } - Optional base_url() const { return m_base_url; } - void set_base_url(Optional base_url) { m_base_url = move(base_url); } + Optional<::URL::URL> base_url() const { return m_base_url; } + void set_base_url(Optional<::URL::URL> base_url) { m_base_url = move(base_url); } bool constructed() const { return m_constructed; } @@ -94,7 +95,7 @@ public: bool has_associated_font_loader(FontLoader& font_loader) const; private: - CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional location); + CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional<::URL::URL> location); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -113,7 +114,7 @@ private: GC::Ptr m_owner_css_rule; - Optional m_base_url; + Optional<::URL::URL> m_base_url; GC::Ptr m_constructor_document; HashTable> m_owning_documents_or_shadow_roots; bool m_constructed { false }; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 1498ee729df..115fabef478 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -225,40 +225,40 @@ public: : m_value(color) { } - SVGPaint(URL::URL const& url) + SVGPaint(::URL::URL const& url) : m_value(url) { } bool is_color() const { return m_value.has(); } - bool is_url() const { return m_value.has(); } + bool is_url() const { return m_value.has<::URL::URL>(); } Color as_color() const { return m_value.get(); } - URL::URL const& as_url() const { return m_value.get(); } + ::URL::URL const& as_url() const { return m_value.get<::URL::URL>(); } private: - Variant m_value; + Variant<::URL::URL, Color> m_value; }; // https://drafts.fxtf.org/css-masking-1/#typedef-mask-reference class MaskReference { public: // TODO: Support other mask types. - MaskReference(URL::URL const& url) + MaskReference(::URL::URL const& url) : m_url(url) { } - URL::URL const& url() const { return m_url; } + ::URL::URL const& url() const { return m_url; } private: - URL::URL m_url; + ::URL::URL m_url; }; // https://drafts.fxtf.org/css-masking/#the-clip-path // TODO: Support clip sources. class ClipPathReference { public: - ClipPathReference(URL::URL const& url) + ClipPathReference(::URL::URL const& url) : m_clip_source(url) { } @@ -270,16 +270,16 @@ public: bool is_basic_shape() const { return m_clip_source.has(); } - bool is_url() const { return m_clip_source.has(); } + bool is_url() const { return m_clip_source.has<::URL::URL>(); } - URL::URL const& url() const { return m_clip_source.get(); } + ::URL::URL const& url() const { return m_clip_source.get<::URL::URL>(); } BasicShapeStyleValue const& basic_shape() const { return *m_clip_source.get(); } private: using BasicShape = NonnullRefPtr; - Variant m_clip_source; + Variant<::URL::URL, BasicShape> m_clip_source; }; struct BackgroundLayerData { diff --git a/Libraries/LibWeb/CSS/Fetch.cpp b/Libraries/LibWeb/CSS/Fetch.cpp index 53a3fa6089a..13c102771a7 100644 --- a/Libraries/LibWeb/CSS/Fetch.cpp +++ b/Libraries/LibWeb/CSS/Fetch.cpp @@ -23,7 +23,7 @@ void fetch_a_style_resource(String const& url_value, CSSStyleSheet const& sheet, auto base = sheet.base_url().value_or(environment_settings.api_base_url()); // 3. Let parsedUrl be the result of the URL parser steps with urlValue’s url and base. If the algorithm returns an error, return. - auto parsed_url = URL::Parser::basic_parse(url_value, base); + auto parsed_url = ::URL::Parser::basic_parse(url_value, base); if (!parsed_url.has_value()) return; diff --git a/Libraries/LibWeb/CSS/ParsedFontFace.cpp b/Libraries/LibWeb/CSS/ParsedFontFace.cpp index 94f11f54d40..e451f03d82e 100644 --- a/Libraries/LibWeb/CSS/ParsedFontFace.cpp +++ b/Libraries/LibWeb/CSS/ParsedFontFace.cpp @@ -38,7 +38,7 @@ Vector ParsedFontFace::sources_from_style_value(CSSStyle [&](FontSourceStyleValue::Local const& local) { sources.empend(extract_font_name(local.name), OptionalNone {}); }, - [&](URL::URL const& url) { + [&](::URL::URL const& url) { // FIXME: tech() sources.empend(url, font_source.format()); }); diff --git a/Libraries/LibWeb/CSS/ParsedFontFace.h b/Libraries/LibWeb/CSS/ParsedFontFace.h index b52bcde095e..7322a64d1e9 100644 --- a/Libraries/LibWeb/CSS/ParsedFontFace.h +++ b/Libraries/LibWeb/CSS/ParsedFontFace.h @@ -19,7 +19,7 @@ namespace Web::CSS { class ParsedFontFace { public: struct Source { - Variant local_or_url; + Variant local_or_url; // FIXME: Do we need to keep this around, or is it only needed to discard unwanted formats during parsing? Optional format; }; diff --git a/Libraries/LibWeb/CSS/Parser/Helpers.cpp b/Libraries/LibWeb/CSS/Parser/Helpers.cpp index cfc3e5487d3..6d194b2c954 100644 --- a/Libraries/LibWeb/CSS/Parser/Helpers.cpp +++ b/Libraries/LibWeb/CSS/Parser/Helpers.cpp @@ -42,7 +42,7 @@ GC::Ref internal_css_realm() return *realm; } -CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const& context, StringView css, Optional location, Vector> media_query_list) +CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const& context, StringView css, Optional<::URL::URL> location, Vector> media_query_list) { if (css.is_empty()) { auto rule_list = CSS::CSSRuleList::create_empty(*context.realm); diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 811f8999c63..b88e81a6995 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -45,14 +45,14 @@ ParsingParams::ParsingParams(JS::Realm& realm, ParsingMode mode) { } -ParsingParams::ParsingParams(JS::Realm& realm, URL::URL url, ParsingMode mode) +ParsingParams::ParsingParams(JS::Realm& realm, ::URL::URL url, ParsingMode mode) : realm(realm) , url(move(url)) , mode(mode) { } -ParsingParams::ParsingParams(DOM::Document const& document, URL::URL url, ParsingMode mode) +ParsingParams::ParsingParams(DOM::Document const& document, ::URL::URL url, ParsingMode mode) : realm(const_cast(document.realm())) , document(&document) , url(move(url)) @@ -86,7 +86,7 @@ Parser::Parser(ParsingParams const& context, Vector tokens) // https://drafts.csswg.org/css-syntax/#parse-stylesheet template -Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream& input, Optional location) +Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream& input, Optional<::URL::URL> location) { // To parse a stylesheet from an input given an optional url location: @@ -119,7 +119,7 @@ Vector Parser::parse_a_stylesheets_contents(TokenStream& input) } // https://drafts.csswg.org/css-syntax/#parse-a-css-stylesheet -CSSStyleSheet* Parser::parse_as_css_stylesheet(Optional location, Vector> media_query_list) +CSSStyleSheet* Parser::parse_as_css_stylesheet(Optional<::URL::URL> location, Vector> media_query_list) { // To parse a CSS stylesheet, first parse a stylesheet. auto const& style_sheet = parse_a_stylesheet(m_token_stream, {}); @@ -1772,8 +1772,8 @@ Parser::ContextType Parser::context_type_for_at_rule(FlyString const& name) return ContextType::Unknown; } -template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional); -template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional); +template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional<::URL::URL>); +template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional<::URL::URL>); template Vector Parser::parse_a_stylesheets_contents(TokenStream& input); template Vector Parser::parse_a_stylesheets_contents(TokenStream& input); @@ -1853,10 +1853,10 @@ bool Parser::is_parsing_svg_presentation_attribute() const // https://www.w3.org/TR/css-values-4/#relative-urls // FIXME: URLs shouldn't be completed during parsing, but when used. -Optional Parser::complete_url(StringView relative_url) const +Optional<::URL::URL> Parser::complete_url(StringView relative_url) const { if (!m_url.is_valid()) - return URL::Parser::basic_parse(relative_url); + return ::URL::Parser::basic_parse(relative_url); return m_url.complete_url(relative_url); } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index d118e6697f3..ad96c2afed9 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -69,13 +69,13 @@ enum class ParsingMode { struct ParsingParams { explicit ParsingParams(ParsingMode = ParsingMode::Normal); explicit ParsingParams(JS::Realm&, ParsingMode = ParsingMode::Normal); - explicit ParsingParams(JS::Realm&, URL::URL, ParsingMode = ParsingMode::Normal); - explicit ParsingParams(DOM::Document const&, URL::URL, ParsingMode = ParsingMode::Normal); + explicit ParsingParams(JS::Realm&, ::URL::URL, ParsingMode = ParsingMode::Normal); + explicit ParsingParams(DOM::Document const&, ::URL::URL, ParsingMode = ParsingMode::Normal); explicit ParsingParams(DOM::Document const&, ParsingMode = ParsingMode::Normal); GC::Ptr realm; GC::Ptr document; - URL::URL url; + ::URL::URL url; ParsingMode mode { ParsingMode::Normal }; }; @@ -89,7 +89,7 @@ class Parser { public: static Parser create(ParsingParams const&, StringView input, StringView encoding = "utf-8"sv); - CSSStyleSheet* parse_as_css_stylesheet(Optional location, Vector> media_query_list = {}); + CSSStyleSheet* parse_as_css_stylesheet(Optional<::URL::URL> location, Vector> media_query_list = {}); struct PropertiesAndCustomProperties { Vector properties; @@ -142,11 +142,11 @@ private: // "Parse a stylesheet" is intended to be the normal parser entry point, for parsing stylesheets. struct ParsedStyleSheet { - Optional location; + Optional<::URL::URL> location; Vector rules; }; template - ParsedStyleSheet parse_a_stylesheet(TokenStream&, Optional location); + ParsedStyleSheet parse_a_stylesheet(TokenStream&, Optional<::URL::URL> location); // "Parse a stylesheet’s contents" is intended for use by the CSSStyleSheet replace() method, and similar, which parse text into the contents of an existing stylesheet. template @@ -276,7 +276,7 @@ private: Optional parse_repeat(Vector const&); Optional parse_track_sizing_function(ComponentValue const&); - Optional parse_url_function(TokenStream&); + Optional<::URL::URL> parse_url_function(TokenStream&); RefPtr parse_url_value(TokenStream&); Optional parse_shape_radius(TokenStream&); @@ -471,11 +471,11 @@ private: JS::Realm& realm() const; bool in_quirks_mode() const; bool is_parsing_svg_presentation_attribute() const; - Optional complete_url(StringView) const; + Optional<::URL::URL> complete_url(StringView) const; GC::Ptr m_document; GC::Ptr m_realm; - URL::URL m_url; + ::URL::URL m_url; ParsingMode m_parsing_mode { ParsingMode::Normal }; Vector m_tokens; @@ -519,7 +519,7 @@ private: namespace Web { -CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const&, StringView, Optional location = {}, Vector> = {}); +CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const&, StringView, Optional<::URL::URL> location = {}, Vector> = {}); CSS::Parser::Parser::PropertiesAndCustomProperties parse_css_style_attribute(CSS::Parser::ParsingParams const&, StringView); Vector parse_css_list_of_descriptors(CSS::Parser::ParsingParams const&, CSS::AtRuleID, StringView); RefPtr parse_css_value(CSS::Parser::ParsingParams const&, StringView, CSS::PropertyID property_id = CSS::PropertyID::Invalid); diff --git a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp index cddd279e031..6896937d359 100644 --- a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp @@ -153,7 +153,7 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) TokenStream tokens { rule.prelude }; tokens.discard_whitespace(); - Optional url = parse_url_function(tokens); + Optional<::URL::URL> url = parse_url_function(tokens); if (!url.has_value() && tokens.next_token().is(Token::Type::String)) url = complete_url(tokens.consume_a_token().token().string()); diff --git a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp index 7fa84481199..a7464d64de2 100644 --- a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp @@ -2014,7 +2014,7 @@ RefPtr Parser::parse_image_value(TokenStream Parser::parse_easing_value(TokenStream& to return nullptr; } -Optional Parser::parse_url_function(TokenStream& tokens) +Optional<::URL::URL> Parser::parse_url_function(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); auto& component_value = tokens.consume_a_token(); - auto convert_string_to_url = [&](StringView url_string) -> Optional { + auto convert_string_to_url = [&](StringView url_string) -> Optional<::URL::URL> { auto url = complete_url(url_string); if (url.has_value()) { transaction.commit(); diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index e58c0dfa56f..dda533237c6 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -185,7 +185,7 @@ StyleComputer::StyleComputer(DOM::Document& document) StyleComputer::~StyleComputer() = default; -FontLoader::FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector urls, Function on_load, Function on_fail) +FontLoader::FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector<::URL::URL> urls, Function on_load, Function on_fail) : m_style_computer(style_computer) , m_family_name(move(family_name)) , m_unicode_ranges(move(unicode_ranges)) @@ -3028,11 +3028,11 @@ Optional StyleComputer::load_font_face(ParsedFontFace const& font_f .slope = font_face.slope().value_or(0), }; - Vector urls; + Vector<::URL::URL> urls; for (auto const& source : font_face.sources()) { // FIXME: These should be loaded relative to the stylesheet URL instead of the document URL. - if (source.local_or_url.has()) - urls.append(*m_document->encoding_parse_url(source.local_or_url.get().to_string())); + if (source.local_or_url.has<::URL::URL>()) + urls.append(*m_document->encoding_parse_url(source.local_or_url.get<::URL::URL>().to_string())); // FIXME: Handle local() } diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index 4fb5ec0cdd1..70747b5909a 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -315,7 +315,7 @@ private: class FontLoader : public ResourceClient { public: - FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector urls, ESCAPING Function on_load = {}, ESCAPING Function on_fail = {}); + FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector<::URL::URL> urls, ESCAPING Function on_load = {}, ESCAPING Function on_fail = {}); virtual ~FontLoader() override; @@ -340,7 +340,7 @@ private: FlyString m_family_name; Vector m_unicode_ranges; RefPtr m_vector_font; - Vector m_urls; + Vector<::URL::URL> m_urls; Function m_on_load; Function m_on_fail; }; diff --git a/Libraries/LibWeb/CSS/StyleSheet.h b/Libraries/LibWeb/CSS/StyleSheet.h index 8f03bcdd7e0..98f6f5d68b0 100644 --- a/Libraries/LibWeb/CSS/StyleSheet.h +++ b/Libraries/LibWeb/CSS/StyleSheet.h @@ -27,8 +27,8 @@ public: Optional href() const; - Optional location() const { return m_location; } - void set_location(Optional location) { m_location = move(location); } + Optional<::URL::URL> location() const { return m_location; } + void set_location(Optional<::URL::URL> location) { m_location = move(location); } String title() const { return m_title; } Optional title_for_bindings() const; @@ -68,7 +68,7 @@ private: GC::Ptr m_owner_node; GC::Ptr m_parent_style_sheet; - Optional m_location; + Optional<::URL::URL> m_location; String m_title; String m_type_string; diff --git a/Libraries/LibWeb/CSS/StyleSheetList.cpp b/Libraries/LibWeb/CSS/StyleSheetList.cpp index 18c49b2db11..be0db396e51 100644 --- a/Libraries/LibWeb/CSS/StyleSheetList.cpp +++ b/Libraries/LibWeb/CSS/StyleSheetList.cpp @@ -60,7 +60,7 @@ void StyleSheetList::add_a_css_style_sheet(CSS::CSSStyleSheet& sheet) } // https://www.w3.org/TR/cssom/#create-a-css-style-sheet -void StyleSheetList::create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet& sheet) +void StyleSheetList::create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional<::URL::URL> location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet& sheet) { // 1. Create a new CSS style sheet object and set its properties as specified. // FIXME: We receive `sheet` from the caller already. This is weird. diff --git a/Libraries/LibWeb/CSS/StyleSheetList.h b/Libraries/LibWeb/CSS/StyleSheetList.h index 317a1865a88..935ab4fca11 100644 --- a/Libraries/LibWeb/CSS/StyleSheetList.h +++ b/Libraries/LibWeb/CSS/StyleSheetList.h @@ -21,7 +21,7 @@ public: void add_a_css_style_sheet(CSS::CSSStyleSheet&); void remove_a_css_style_sheet(CSS::CSSStyleSheet&); - void create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet&); + void create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional<::URL::URL> location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet&); Vector> const& sheets() const { return m_sheets; } Vector>& sheets() { return m_sheets; } diff --git a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp index dd3b15db4bf..ee8cb1641db 100644 --- a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp @@ -34,7 +34,7 @@ String FontSourceStyleValue::to_string(SerializationMode) const builder.append(')'); return builder.to_string_without_validation(); }, - [this](URL::URL const& url) { + [this](::URL::URL const& url) { // [ format()]? [ tech( #)]? // FIXME: tech() StringBuilder builder; @@ -59,8 +59,8 @@ bool FontSourceStyleValue::properties_equal(FontSourceStyleValue const& other) c } return false; }, - [&other](URL::URL const& url) { - if (auto* other_url = other.m_source.get_pointer()) { + [&other](::URL::URL const& url) { + if (auto* other_url = other.m_source.get_pointer<::URL::URL>()) { return url == *other_url; } return false; diff --git a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h index 732f990bdaa..f088947afff 100644 --- a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h @@ -16,7 +16,7 @@ public: struct Local { NonnullRefPtr name; }; - using Source = Variant; + using Source = Variant; static ValueComparingNonnullRefPtr create(Source source, Optional format) { diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp index c35ddf8fcde..1e12e5c1569 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp @@ -20,7 +20,7 @@ namespace Web::CSS { -ImageStyleValue::ImageStyleValue(URL::URL const& url) +ImageStyleValue::ImageStyleValue(::URL::URL const& url) : AbstractImageStyleValue(Type::Image) , m_url(url) { diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h index d643635efe0..9359897dafb 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h @@ -25,7 +25,7 @@ class ImageStyleValue final using Base = AbstractImageStyleValue; public: - static ValueComparingNonnullRefPtr create(URL::URL const& url) + static ValueComparingNonnullRefPtr create(::URL::URL const& url) { return adopt_ref(*new (nothrow) ImageStyleValue(url)); } @@ -53,14 +53,14 @@ public: GC::Ptr image_data() const; private: - ImageStyleValue(URL::URL const&); + ImageStyleValue(::URL::URL const&); GC::Ptr m_resource_request; void animate(); Gfx::ImmutableBitmap const* bitmap(size_t frame_index, Gfx::IntSize = {}) const; - URL::URL m_url; + ::URL::URL m_url; WeakPtr m_document; size_t m_current_frame_index { 0 }; diff --git a/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h index 6a7844aa153..8e4b1f45a3f 100644 --- a/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h @@ -14,14 +14,14 @@ namespace Web::CSS { class URLStyleValue final : public StyleValueWithDefaultOperators { public: - static ValueComparingNonnullRefPtr create(URL::URL const& url) + static ValueComparingNonnullRefPtr create(::URL::URL const& url) { return adopt_ref(*new (nothrow) URLStyleValue(url)); } virtual ~URLStyleValue() override = default; - URL::URL const& url() const { return m_url; } + ::URL::URL const& url() const { return m_url; } bool properties_equal(URLStyleValue const& other) const { return m_url == other.m_url; } @@ -31,13 +31,13 @@ public: } private: - URLStyleValue(URL::URL const& url) + URLStyleValue(::URL::URL const& url) : StyleValueWithDefaultOperators(Type::URL) , m_url(url) { } - URL::URL m_url; + ::URL::URL m_url; }; } diff --git a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp index a37b421c82c..03d2e373c67 100644 --- a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp @@ -490,7 +490,7 @@ void HTMLLinkElement::process_stylesheet_resource(bool success, Fetch::Infrastru m_loaded_style_sheet = parse_css_stylesheet(CSS::Parser::ParsingParams(document(), *response.url()), decoded_string); if (m_loaded_style_sheet) { - Optional location; + Optional<::URL::URL> location; if (!response.url_list().is_empty()) location = response.url_list().first(); From 7216c6b0502237bd11e80af09212e22b7d497b94 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 8 Apr 2025 14:33:01 +0100 Subject: [PATCH 04/83] LibWeb/CSS: Parse `` as a new CSS::URL type Our previous approach to `` had a couple of issues: - We'd complete the URL during parsing, when we should actually keep it as the original string until it's used. - There's nowhere for us to store ``s on a `URL::URL`. So, `CSS::URL` is a solution to this. It holds the original URL string, and later will also hold any modifiers. This commit parses all ``s as `CSS::URL`, but then converts it into a `URL::URL`, so no user code is changed. These will be modified in subsequent commits. For `@namespace`, we were never supposed to complete the URL at all, so this makes that more correct already. However, in practice all `@namespace`s are absolute URLs already, so this should have no observable effects. --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/CSS/Parser/Parser.h | 3 +- Libraries/LibWeb/CSS/Parser/RuleParsing.cpp | 18 ++++++-- Libraries/LibWeb/CSS/Parser/ValueParsing.cpp | 47 +++++++++++--------- Libraries/LibWeb/CSS/URL.cpp | 31 +++++++++++++ Libraries/LibWeb/CSS/URL.h | 27 +++++++++++ Libraries/LibWeb/Forward.h | 1 + 7 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 Libraries/LibWeb/CSS/URL.cpp create mode 100644 Libraries/LibWeb/CSS/URL.h diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 62f8c2c9985..55ca653d986 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -195,6 +195,7 @@ set(SOURCES CSS/Time.cpp CSS/Transformation.cpp CSS/TransitionEvent.cpp + CSS/URL.cpp CSS/VisualViewport.cpp Cookie/Cookie.cpp Cookie/ParsedCookie.cpp diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index ad96c2afed9..0931aa232f9 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -32,6 +32,7 @@ #include #include #include +#include #include namespace Web::CSS::Parser { @@ -276,7 +277,7 @@ private: Optional parse_repeat(Vector const&); Optional parse_track_sizing_function(ComponentValue const&); - Optional<::URL::URL> parse_url_function(TokenStream&); + Optional parse_url_function(TokenStream&); RefPtr parse_url_value(TokenStream&); Optional parse_shape_radius(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp index 6896937d359..4f511e1ade5 100644 --- a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp @@ -153,15 +153,22 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) TokenStream tokens { rule.prelude }; tokens.discard_whitespace(); - Optional<::URL::URL> url = parse_url_function(tokens); + Optional url = parse_url_function(tokens); if (!url.has_value() && tokens.next_token().is(Token::Type::String)) - url = complete_url(tokens.consume_a_token().token().string()); + url = URL { tokens.consume_a_token().token().string().to_string() }; if (!url.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string()); return {}; } + // FIXME: Stop completing the URL here + auto resolved_url = complete_url(url->url()); + if (!resolved_url.has_value()) { + dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to complete `{}` as URL.", url->url()); + return {}; + } + tokens.discard_whitespace(); // FIXME: Implement layer support. RefPtr supports {}; @@ -191,7 +198,7 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) return {}; } - return CSSImportRule::create(url.value(), const_cast(*document()), supports, move(media_query_list)); + return CSSImportRule::create(resolved_url.release_value(), const_cast(*document()), supports, move(media_query_list)); } Optional Parser::parse_layer_name(TokenStream& tokens, AllowBlankLayerName allow_blank_layer_name) @@ -435,7 +442,10 @@ GC::Ptr Parser::convert_to_namespace_rule(AtRule const& rule) FlyString namespace_uri; if (auto url = parse_url_function(tokens); url.has_value()) { - namespace_uri = url.value().to_string(); + // "A URI string parsed from the URI syntax must be treated as a literal string: as with the STRING syntax, no + // URI-specific normalization is applied." + // https://drafts.csswg.org/css-namespaces/#syntax + namespace_uri = url->url(); } else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) { namespace_uri = url_token.token().string(); } else { diff --git a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp index a7464d64de2..16ce3677fd1 100644 --- a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -2014,9 +2013,13 @@ RefPtr Parser::parse_image_value(TokenStreamurl().starts_with('#')) { + // FIXME: Stop completing the URL here + auto completed_url = complete_url(url->url()); + if (completed_url.has_value()) { + tokens.discard_a_mark(); + return ImageStyleValue::create(completed_url.release_value()); + } } tokens.restore_a_mark(); return nullptr; @@ -2562,24 +2565,16 @@ RefPtr Parser::parse_easing_value(TokenStream& to return nullptr; } -Optional<::URL::URL> Parser::parse_url_function(TokenStream& tokens) +Optional Parser::parse_url_function(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); - auto& component_value = tokens.consume_a_token(); - - auto convert_string_to_url = [&](StringView url_string) -> Optional<::URL::URL> { - auto url = complete_url(url_string); - if (url.has_value()) { - transaction.commit(); - return url; - } - return {}; - }; + auto const& component_value = tokens.consume_a_token(); if (component_value.is(Token::Type::Url)) { - auto url_string = component_value.token().url(); - return convert_string_to_url(url_string); + transaction.commit(); + return URL { component_value.token().url().to_string() }; } + if (component_value.is_function("url"sv)) { auto const& function_values = component_value.function().value; // FIXME: Handle url-modifiers. https://www.w3.org/TR/css-values-4/#url-modifiers @@ -2588,8 +2583,8 @@ Optional<::URL::URL> Parser::parse_url_function(TokenStream& tok if (value.is(Token::Type::Whitespace)) continue; if (value.is(Token::Type::String)) { - auto url_string = value.token().string(); - return convert_string_to_url(url_string); + transaction.commit(); + return URL { value.token().string().to_string() }; } break; } @@ -2603,7 +2598,11 @@ RefPtr Parser::parse_url_value(TokenStream& token auto url = parse_url_function(tokens); if (!url.has_value()) return nullptr; - return URLStyleValue::create(*url); + // FIXME: Stop completing the URL here + auto completed_url = complete_url(url->url()); + if (!completed_url.has_value()) + return nullptr; + return URLStyleValue::create(completed_url.release_value()); } // https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius @@ -3681,7 +3680,11 @@ RefPtr Parser::parse_font_source_value(TokenStream [ format()]? [ tech( #)]? auto url = parse_url_function(tokens); - if (!url.has_value() || !url->is_valid()) + if (!url.has_value()) + return nullptr; + // FIXME: Stop completing the URL here + auto completed_url = complete_url(url->url()); + if (!completed_url.has_value()) return nullptr; Optional format; @@ -3719,7 +3722,7 @@ RefPtr Parser::parse_font_source_value(TokenStream#)]? transaction.commit(); - return FontSourceStyleValue::create(url.release_value(), move(format)); + return FontSourceStyleValue::create(completed_url.release_value(), move(format)); } NonnullRefPtr Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved) diff --git a/Libraries/LibWeb/CSS/URL.cpp b/Libraries/LibWeb/CSS/URL.cpp new file mode 100644 index 00000000000..a634b6128bc --- /dev/null +++ b/Libraries/LibWeb/CSS/URL.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::CSS { + +URL::URL(String url) + : m_url(move(url)) +{ +} + +// https://drafts.csswg.org/cssom-1/#serialize-a-url +String URL::to_string() const +{ + // To serialize a URL means to create a string represented by "url(", followed by the serialization of the URL as a string, followed by ")". + StringBuilder builder; + builder.append("url("sv); + serialize_a_string(builder, m_url); + builder.append(')'); + + return builder.to_string_without_validation(); +} + +bool URL::operator==(URL const&) const = default; + +} diff --git a/Libraries/LibWeb/CSS/URL.h b/Libraries/LibWeb/CSS/URL.h new file mode 100644 index 00000000000..9380ad2e226 --- /dev/null +++ b/Libraries/LibWeb/CSS/URL.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS { + +// https://drafts.csswg.org/css-values-4/#urls +class URL { +public: + URL(String url); + + String const& url() const { return m_url; } + + String to_string() const; + bool operator==(URL const&) const; + +private: + String m_url; +}; + +} diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 557f007dafc..8a9eca06696 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -269,6 +269,7 @@ class TransformationStyleValue; class TransitionStyleValue; class UnicodeRangeStyleValue; class UnresolvedStyleValue; +class URL; class URLStyleValue; class VisualViewport; From bc02e3e9a9aaef7598862f26307ae51607c1c49b Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 8 Apr 2025 15:33:53 +0100 Subject: [PATCH 05/83] LibWeb/CSS: Pass location to parse_a_stylesheet() --- Libraries/LibWeb/CSS/Parser/Parser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index b88e81a6995..5b4f7a2c422 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -122,7 +122,7 @@ Vector Parser::parse_a_stylesheets_contents(TokenStream& input) CSSStyleSheet* Parser::parse_as_css_stylesheet(Optional<::URL::URL> location, Vector> media_query_list) { // To parse a CSS stylesheet, first parse a stylesheet. - auto const& style_sheet = parse_a_stylesheet(m_token_stream, {}); + auto const& style_sheet = parse_a_stylesheet(m_token_stream, location); // Interpret all of the resulting top-level qualified rules as style rules, defined below. GC::RootVector rules(realm().heap()); From a8ab4d64c462c47c10da51fa12780d418e3a6127 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 8 Apr 2025 15:37:20 +0100 Subject: [PATCH 06/83] LibWeb/DOM: Use document's URL as location for inline stylesheets This is ad-hoc, and the spec doesn't seem to tell us what to actually do here. Without this, following the spec steps for loading relative `@import` URLs from a ` +
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/cssom/cssimportrule.html b/Tests/LibWeb/Text/input/wpt-import/css/cssom/cssimportrule.html new file mode 100644 index 00000000000..1a87e7be7c3 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/cssom/cssimportrule.html @@ -0,0 +1,124 @@ + + + + CSSOM CSSRule CSSImportRule interface + + + + + + + + + + + + +
+ + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/cssom/support/a-green.css b/Tests/LibWeb/Text/input/wpt-import/css/cssom/support/a-green.css new file mode 100644 index 00000000000..b0dbb071d5b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/cssom/support/a-green.css @@ -0,0 +1 @@ +.a { color: green; } From ca0890ce16477542b30fa0444c95afa8a2da6275 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 9 Apr 2025 11:33:18 +0100 Subject: [PATCH 10/83] LibWeb/CSS: Only try to fetch `@import`s with a parent style sheet When `CSSRuleList::remove_a_css_rule()` is called, the removed rule has its parent style sheet set to null. We shouldn't try to fetch an import in this case. --- Libraries/LibWeb/CSS/CSSImportRule.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/CSSImportRule.cpp b/Libraries/LibWeb/CSS/CSSImportRule.cpp index adac6310417..e2610237cb3 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.cpp +++ b/Libraries/LibWeb/CSS/CSSImportRule.cpp @@ -56,7 +56,10 @@ void CSSImportRule::set_parent_style_sheet(CSSStyleSheet* parent_style_sheet) // Crude detection of whether we're already fetching. if (m_style_sheet || m_document_load_event_delayer.has_value()) return; - fetch(); + + // Only try to fetch if we now have a parent + if (parent_style_sheet) + fetch(); } // https://www.w3.org/TR/cssom/#serialize-a-css-rule From 0f42d5ec3e894dd15c6563b6bc0266973f1cb6cf Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Tue, 8 Apr 2025 18:16:38 +0100 Subject: [PATCH 11/83] LibWeb/CSS: Don't resolve `@import` URLs until they are used The regression in the "conditional-CSSGroupingRule" test is we now fail the "inserting an `@import`" subtests differently and the subtests aren't independent. Specifically, we don't yet implement the checks in `CSSRuleList::insert_a_css_rule()` that reject certain rules from being inserted. Previously we didn't insert the `@import` rule because we failed to parse its relative URL. Now we parse it correctly, we end up inserting it. --- Libraries/LibWeb/CSS/CSSImportRule.cpp | 25 +++++++++++-------- Libraries/LibWeb/CSS/CSSImportRule.h | 15 ++++++----- Libraries/LibWeb/CSS/Parser/RuleParsing.cpp | 9 +------ Libraries/LibWeb/Dump.cpp | 2 +- Services/WebContent/PageClient.cpp | 2 +- .../css/CSSImportRule-supportsText.txt | 4 +-- .../parsing/supports-import-parsing.txt | 12 ++++----- .../js/conditional-CSSGroupingRule.txt | 12 ++++----- .../wpt-import/css/cssom/cssimportrule.txt | 6 ++--- 9 files changed, 41 insertions(+), 46 deletions(-) diff --git a/Libraries/LibWeb/CSS/CSSImportRule.cpp b/Libraries/LibWeb/CSS/CSSImportRule.cpp index e2610237cb3..ea5773c50bb 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.cpp +++ b/Libraries/LibWeb/CSS/CSSImportRule.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -17,18 +16,19 @@ #include #include #include +#include #include namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSImportRule); -GC::Ref CSSImportRule::create(JS::Realm& realm, ::URL::URL url, GC::Ptr document, RefPtr supports, Vector> media_query_list) +GC::Ref CSSImportRule::create(JS::Realm& realm, URL url, GC::Ptr document, RefPtr supports, Vector> media_query_list) { return realm.create(realm, move(url), document, move(supports), move(media_query_list)); } -CSSImportRule::CSSImportRule(JS::Realm& realm, ::URL::URL url, GC::Ptr document, RefPtr supports, Vector> media_query_list) +CSSImportRule::CSSImportRule(JS::Realm& realm, URL url, GC::Ptr document, RefPtr supports, Vector> media_query_list) : CSSRule(realm, Type::Import) , m_url(move(url)) , m_document(document) @@ -72,7 +72,7 @@ String CSSImportRule::serialized() const builder.append("@import "sv); // 2. The result of performing serialize a URL on the rule’s location. - serialize_a_url(builder, m_url.to_string()); + builder.append(m_url.to_string()); // AD-HOC: Serialize the rule's supports condition if it exists. // This isn't currently specified, but major browsers include this in their serialization of import rules @@ -106,15 +106,18 @@ void CSSImportRule::fetch() // 3. Let parsedUrl be the result of the URL parser steps with rule’s URL and parentStylesheet’s location. // If the algorithm returns an error, return. [CSSOM] - // FIXME: Stop producing a URL::URL when parsing the @import - auto parsed_url = url().to_string(); + auto parsed_url = DOMURL::parse(href(), parent_style_sheet.location()); + if (!parsed_url.has_value()) { + dbgln("Unable to parse @import url `{}` parent location `{}` as a URL.", href(), parent_style_sheet.location()); + return; + } // FIXME: Figure out the "correct" way to delay the load event. m_document_load_event_delayer.emplace(*m_document); // 4. Fetch a style resource from parsedUrl, with stylesheet parentStylesheet, destination "style", CORS mode "no-cors", and processResponse being the following steps given response response and byte stream, null or failure byteStream: - fetch_a_style_resource(parsed_url, parent_style_sheet, Fetch::Infrastructure::Request::Destination::Style, CorsMode::NoCors, - [strong_this = GC::Ref { *this }, parent_style_sheet = GC::Ref { parent_style_sheet }](auto response, auto maybe_byte_stream) { + fetch_a_style_resource(parsed_url->to_string(), parent_style_sheet, Fetch::Infrastructure::Request::Destination::Style, CorsMode::NoCors, + [strong_this = GC::Ref { *this }, parent_style_sheet = GC::Ref { parent_style_sheet }, parsed_url = parsed_url.value()](auto response, auto maybe_byte_stream) { // AD-HOC: Stop delaying the load event. ScopeGuard guard = [strong_this] { strong_this->m_document_load_event_delayer.clear(); @@ -141,19 +144,19 @@ void CSSImportRule::fetch() auto encoding = "utf-8"sv; auto maybe_decoder = TextCodec::decoder_for(encoding); if (!maybe_decoder.has_value()) { - dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Unsupported encoding: {}", strong_this->url(), encoding); + dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Unsupported encoding: {}", parsed_url, encoding); return; } auto& decoder = maybe_decoder.release_value(); auto decoded_or_error = TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(decoder, *byte_stream); if (decoded_or_error.is_error()) { - dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Encoding was: {}", strong_this->url(), encoding); + dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Encoding was: {}", parsed_url, encoding); return; } auto decoded = decoded_or_error.release_value(); - auto* imported_style_sheet = parse_css_stylesheet(Parser::ParsingParams(*strong_this->m_document, strong_this->url()), decoded, strong_this->url(), strong_this->m_media_query_list); + auto* imported_style_sheet = parse_css_stylesheet(Parser::ParsingParams(*strong_this->m_document, parsed_url), decoded, parsed_url, strong_this->m_media_query_list); // 5. Set importedStylesheet’s origin-clean flag to parentStylesheet’s origin-clean flag. imported_style_sheet->set_origin_clean(parent_style_sheet->is_origin_clean()); diff --git a/Libraries/LibWeb/CSS/CSSImportRule.h b/Libraries/LibWeb/CSS/CSSImportRule.h index 72df3a1d2cb..a9f3abb3480 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.h +++ b/Libraries/LibWeb/CSS/CSSImportRule.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, the SerenityOS developers. - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2022, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause @@ -8,9 +8,9 @@ #pragma once -#include #include #include +#include #include namespace Web::CSS { @@ -21,13 +21,12 @@ class CSSImportRule final GC_DECLARE_ALLOCATOR(CSSImportRule); public: - [[nodiscard]] static GC::Ref create(JS::Realm&, ::URL::URL, GC::Ptr, RefPtr, Vector>); + [[nodiscard]] static GC::Ref create(JS::Realm&, URL, GC::Ptr, RefPtr, Vector>); virtual ~CSSImportRule() = default; - ::URL::URL const& url() const { return m_url; } - // FIXME: This should return only the specified part of the url. eg, "stuff/foo.css", not "https://example.com/stuff/foo.css". - String href() const { return m_url.to_string(); } + URL const& url() const { return m_url; } + String href() const { return m_url.url(); } CSSStyleSheet* loaded_style_sheet() { return m_style_sheet; } CSSStyleSheet const* loaded_style_sheet() const { return m_style_sheet; } @@ -37,7 +36,7 @@ public: Optional supports_text() const; private: - CSSImportRule(JS::Realm&, ::URL::URL, GC::Ptr, RefPtr, Vector>); + CSSImportRule(JS::Realm&, URL, GC::Ptr, RefPtr, Vector>); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -49,7 +48,7 @@ private: void fetch(); void set_style_sheet(GC::Ref); - ::URL::URL m_url; + URL m_url; GC::Ptr m_document; RefPtr m_supports; Vector> m_media_query_list; diff --git a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp index aed7a88923b..de02d4ba10f 100644 --- a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp @@ -162,13 +162,6 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) return {}; } - // FIXME: Stop completing the URL here - auto resolved_url = complete_url(url->url()); - if (!resolved_url.has_value()) { - dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to complete `{}` as URL.", url->url()); - return {}; - } - tokens.discard_whitespace(); // FIXME: Implement layer support. RefPtr supports {}; @@ -198,7 +191,7 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) return {}; } - return CSSImportRule::create(realm(), resolved_url.release_value(), const_cast(m_document.ptr()), supports, move(media_query_list)); + return CSSImportRule::create(realm(), url.release_value(), const_cast(m_document.ptr()), supports, move(media_query_list)); } Optional Parser::parse_layer_name(TokenStream& tokens, AllowBlankLayerName allow_blank_layer_name) diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index 2803277488f..a8fe444eb28 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -790,7 +790,7 @@ void dump_font_face_rule(StringBuilder& builder, CSS::CSSFontFaceRule const& rul void dump_import_rule(StringBuilder& builder, CSS::CSSImportRule const& rule, int indent_levels) { indent(builder, indent_levels); - builder.appendff(" Document URL: {}\n", rule.url()); + builder.appendff(" Document URL: {}\n", rule.url().to_string()); } void dump_layer_block_rule(StringBuilder& builder, CSS::CSSLayerBlockRule const& layer_block, int indent_levels) diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index 1749b33db44..86c36aad0bb 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -848,7 +848,7 @@ static void gather_style_sheets(Vector& results, // We can gather this anyway, and hope it loads later results.append({ .type = Web::CSS::StyleSheetIdentifier::Type::ImportRule, - .url = import_rule->url().to_string(), + .url = import_rule->href(), }); } } diff --git a/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt b/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt index 52162806d0d..d9ef6f2cb1e 100644 --- a/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt +++ b/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt @@ -1,4 +1,4 @@ -cssText: @import url("https://something.invalid/") supports(foo: bar); +cssText: @import url("https://something.invalid") supports(foo: bar); supportsText: foo: bar -cssText: @import url("https://something.invalid/") supports((display: block) and (foo: bar)); +cssText: @import url("https://something.invalid") supports((display: block) and (foo: bar)); supportsText: (display: block) and (foo: bar) diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt index 13f9fa737db..b58337dfe22 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt @@ -2,17 +2,17 @@ Harness status: OK Found 22 tests -5 Pass -17 Fail +9 Pass +13 Fail Pass @import url("nonexist.css") supports(); should be an invalid import rule due to an invalid supports() declaration Pass @import url("nonexist.css") supports(foo: bar); should be an invalid import rule due to an invalid supports() declaration Fail @import url("nonexist.css") supports(display:block); should be a valid supports() import rule Fail @import url("nonexist.css") supports((display:flex)); should be a valid supports() import rule Fail @import url("nonexist.css") supports(not (display: flex)); should be a valid supports() import rule -Fail @import url("nonexist.css") supports((display: flex) and (display: block)); should be a valid supports() import rule -Fail @import url("nonexist.css") supports((display: flex) or (display: block)); should be a valid supports() import rule -Fail @import url("nonexist.css") supports((display: flex) or (foo: bar)); should be a valid supports() import rule -Fail @import url("nonexist.css") supports(display: block !important); should be a valid supports() import rule +Pass @import url("nonexist.css") supports((display: flex) and (display: block)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports((display: flex) or (display: block)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports((display: flex) or (foo: bar)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports(display: block !important); should be a valid supports() import rule Pass @import url("nonexist.css") layer supports(); should be an invalid import rule due to an invalid supports() declaration Pass @import url("nonexist.css") layer supports(foo: bar); should be an invalid import rule due to an invalid supports() declaration Fail @import url("nonexist.css") layer(A) supports((display: flex) or (foo: bar)); should be a valid supports() import rule diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt index e45e3813268..ea806d930f2 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt @@ -2,14 +2,14 @@ Harness status: OK Found 34 tests -30 Pass -4 Fail +26 Pass +8 Fail Pass @media is CSSGroupingRule Pass @media rule type Pass Empty @media rule length Fail insertRule of @import into @media -Pass insertRule into empty @media at bad index -Pass insertRule into @media updates length +Fail insertRule into empty @media at bad index +Fail insertRule into @media updates length Pass insertRule of valid @media into @media Pass insertRule of valid style rule into @media Fail insertRule of invalid @media into @media @@ -25,8 +25,8 @@ Pass @supports is CSSGroupingRule Pass @supports rule type Pass Empty @supports rule length Fail insertRule of @import into @supports -Pass insertRule into empty @supports at bad index -Pass insertRule into @supports updates length +Fail insertRule into empty @supports at bad index +Fail insertRule into @supports updates length Pass insertRule of valid @media into @supports Pass insertRule of valid style rule into @supports Fail insertRule of invalid @media into @supports diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt b/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt index 17f7264c257..7bc6e91ba48 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt @@ -2,12 +2,12 @@ Harness status: OK Found 11 tests -7 Pass -4 Fail +8 Pass +3 Fail Pass CSSRule and CSSImportRule types Pass Type of CSSRule#type and constant values Pass Existence and writability of CSSRule attributes -Fail Values of CSSRule attributes +Pass Values of CSSRule attributes Pass Existence and writability of CSSImportRule attributes Fail Values of CSSImportRule attributes Fail CSSImportRule : MediaList mediaText attribute should be updated due to [PutForwards] From 848a250b29f4581eb2ce486109d0697137d05a42 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 9 Apr 2025 16:36:07 +0100 Subject: [PATCH 12/83] LibWeb/CSS: Mark CSSImportRule.media as nullable See the linked spec issue for more details. The MediaList can be null internally, and this was upsetting GCC as it meant our bindings code was dereferencing a null pointer. --- Libraries/LibWeb/CSS/CSSImportRule.idl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/CSSImportRule.idl b/Libraries/LibWeb/CSS/CSSImportRule.idl index 2256427abd2..c16a272a42b 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.idl +++ b/Libraries/LibWeb/CSS/CSSImportRule.idl @@ -6,7 +6,8 @@ [Exposed=Window] interface CSSImportRule : CSSRule { readonly attribute USVString href; - [SameObject, PutForwards=mediaText] readonly attribute MediaList media; + // AD-HOC: media is null if styleSheet is null. Spec issue: https://github.com/w3c/csswg-drafts/issues/12063 + [SameObject, PutForwards=mediaText] readonly attribute MediaList? media; [SameObject, ImplementedAs=style_sheet_for_bindings] readonly attribute CSSStyleSheet? styleSheet; [FIXME] readonly attribute CSSOMString? layerName; readonly attribute CSSOMString? supportsText; From 8beade51e08293f32d0bed29a555a5533d4a22db Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 9 Apr 2025 16:36:30 +0100 Subject: [PATCH 13/83] LibWeb/CSS: Make it clear that StyleSheet::media() is never null --- Libraries/LibWeb/CSS/StyleSheet.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/StyleSheet.h b/Libraries/LibWeb/CSS/StyleSheet.h index 98f6f5d68b0..b85d97fc5e5 100644 --- a/Libraries/LibWeb/CSS/StyleSheet.h +++ b/Libraries/LibWeb/CSS/StyleSheet.h @@ -36,7 +36,7 @@ public: void set_type(String type) { m_type_string = move(type); } - MediaList* media() const + GC::Ref media() const { return m_media; } From 9321ad04c0e58cec493072288dfdbbb3120e7780 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 18:26:13 +0200 Subject: [PATCH 14/83] LibWeb/IDB: Add internal Index object --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/Forward.h | 1 + Libraries/LibWeb/IndexedDB/Internal/Index.cpp | 42 ++++++++++++ Libraries/LibWeb/IndexedDB/Internal/Index.h | 67 +++++++++++++++++++ .../LibWeb/IndexedDB/Internal/ObjectStore.cpp | 1 + .../LibWeb/IndexedDB/Internal/ObjectStore.h | 7 ++ 6 files changed, 119 insertions(+) create mode 100644 Libraries/LibWeb/IndexedDB/Internal/Index.cpp create mode 100644 Libraries/LibWeb/IndexedDB/Internal/Index.h diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 55ca653d986..b9f3eab6734 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -567,6 +567,7 @@ set(SOURCES IndexedDB/IDBVersionChangeEvent.cpp IndexedDB/Internal/Algorithms.cpp IndexedDB/Internal/Database.cpp + IndexedDB/Internal/Index.cpp IndexedDB/Internal/Key.cpp IndexedDB/Internal/ObjectStore.cpp IndexedDB/Internal/RequestList.cpp diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 8a9eca06696..0271a0c1d39 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -625,6 +625,7 @@ class IDBOpenDBRequest; class IDBRequest; class IDBTransaction; class IDBVersionChangeEvent; +class Index; class ObjectStore; class RequestList; } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Index.cpp b/Libraries/LibWeb/IndexedDB/Internal/Index.cpp new file mode 100644 index 00000000000..38ea6e1e27e --- /dev/null +++ b/Libraries/LibWeb/IndexedDB/Internal/Index.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, stelar7 + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::IndexedDB { + +GC_DEFINE_ALLOCATOR(Index); + +Index::~Index() = default; + +GC::Ref Index::create(JS::Realm& realm, GC::Ref store, String name, KeyPath const& key_path, bool unique, bool multi_entry) +{ + return realm.create(store, name, key_path, unique, multi_entry); +} + +Index::Index(GC::Ref store, String name, KeyPath const& key_path, bool unique, bool multi_entry) + : m_object_store(store) + , m_name(move(name)) + , m_unique(unique) + , m_multi_entry(multi_entry) + , m_key_path(key_path) +{ + store->add_index(*this); +} + +void Index::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_object_store); + + for (auto& record : m_records) { + visitor.visit(record.key); + visitor.visit(record.value); + } +} + +} diff --git a/Libraries/LibWeb/IndexedDB/Internal/Index.h b/Libraries/LibWeb/IndexedDB/Internal/Index.h new file mode 100644 index 00000000000..e984ffbc1b4 --- /dev/null +++ b/Libraries/LibWeb/IndexedDB/Internal/Index.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, stelar7 + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::IndexedDB { + +using KeyPath = Variant>; + +// https://w3c.github.io/IndexedDB/#index-list-of-records +struct IndexRecord { + GC::Ref key; + GC::Ref value; +}; + +// https://w3c.github.io/IndexedDB/#index-construct +class Index : public JS::Cell { + GC_CELL(Index, JS::Cell); + GC_DECLARE_ALLOCATOR(Index); + +public: + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, String, KeyPath const&, bool, bool); + virtual ~Index(); + + void set_name(String name) { m_name = move(name); } + [[nodiscard]] String name() const { return m_name; } + [[nodiscard]] bool unique() const { return m_unique; } + [[nodiscard]] bool multi_entry() const { return m_multi_entry; } + [[nodiscard]] GC::Ref object_store() const { return m_object_store; } + [[nodiscard]] AK::ReadonlySpan records() const { return m_records; } + [[nodiscard]] KeyPath const& key_path() const { return m_key_path; } + +protected: + virtual void visit_edges(Visitor&) override; + +private: + Index(GC::Ref, String, KeyPath const&, bool, bool); + + // An index [...] has a referenced object store. + GC::Ref m_object_store; + + // The index has a list of records which hold the data stored in the index. + Vector m_records; + + // An index has a name, which is a name. At any one time, the name is unique within index’s referenced object store. + String m_name; + + // An index has a unique flag. When true, the index enforces that no two records in the index has the same key. + bool m_unique { false }; + + // An index has a multiEntry flag. This flag affects how the index behaves when the result of evaluating the index’s key path yields an array key. + bool m_multi_entry { false }; + + // The keys are derived from the referenced object store’s values using a key path. + KeyPath m_key_path; +}; + +} diff --git a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp index 2169030f821..3fe00e26cd2 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp @@ -32,6 +32,7 @@ void ObjectStore::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_database); + visitor.visit(m_indexes); } } diff --git a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h index 99b7de285ba..d7f400e37bc 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace Web::IndexedDB { @@ -39,6 +40,9 @@ public: GC::Ref database() const { return m_database; } + void add_index(GC::Ref index) { m_indexes.append(index); } + ReadonlySpan> index_set() const { return m_indexes; } + protected: virtual void visit_edges(Visitor&) override; @@ -48,6 +52,9 @@ private: // AD-HOC: An ObjectStore needs to know what Database it belongs to... GC::Ref m_database; + // AD-HOC: An Index has referenced ObjectStores, we also need the reverse mapping + Vector> m_indexes; + // An object store has a name, which is a name. At any one time, the name is unique within the database to which it belongs. String m_name; From a235dd43002e7a5cef4fa2ed0248e71ea77bd235 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 18:36:39 +0200 Subject: [PATCH 15/83] LibWeb/IDB: Fillout IDBIndex attributes --- Libraries/LibWeb/IndexedDB/IDBIndex.cpp | 74 ++++++++++++++++++++++++- Libraries/LibWeb/IndexedDB/IDBIndex.h | 27 ++++++++- Libraries/LibWeb/IndexedDB/IDBIndex.idl | 10 ++-- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.cpp b/Libraries/LibWeb/IndexedDB/IDBIndex.cpp index 9d11c0ec8dd..a62ea6e5e31 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -15,14 +16,17 @@ GC_DEFINE_ALLOCATOR(IDBIndex); IDBIndex::~IDBIndex() = default; -IDBIndex::IDBIndex(JS::Realm& realm) +IDBIndex::IDBIndex(JS::Realm& realm, GC::Ref index, GC::Ref object_store) : PlatformObject(realm) + , m_index(index) + , m_object_store_handle(object_store) + , m_name(index->name()) { } -GC::Ref IDBIndex::create(JS::Realm& realm) +GC::Ref IDBIndex::create(JS::Realm& realm, GC::Ref index, GC::Ref object_store) { - return realm.create(realm); + return realm.create(realm, index, object_store); } void IDBIndex::initialize(JS::Realm& realm) @@ -31,4 +35,68 @@ void IDBIndex::initialize(JS::Realm& realm) WEB_SET_PROTOTYPE_FOR_INTERFACE(IDBIndex); } +void IDBIndex::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_index); + visitor.visit(m_object_store_handle); +} + +// https://w3c.github.io/IndexedDB/#dom-idbindex-name +WebIDL::ExceptionOr IDBIndex::set_name(String const& value) +{ + auto& realm = this->realm(); + + // 1. Let name be the given value. + auto const& name = value; + + // 2. Let transaction be this’s transaction. + auto transaction = this->transaction(); + + // 3. Let index be this’s index. + auto index = this->index(); + + // 4. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException. + if (!transaction->is_upgrade_transaction()) + return WebIDL::InvalidStateError::create(realm, "Transaction is not an upgrade transaction"_string); + + // 5. If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException. + if (transaction->state() != IDBTransaction::TransactionState::Active) + return WebIDL::TransactionInactiveError::create(realm, "Transaction is not active"_string); + + // FIXME: 6. If index or index’s object store has been deleted, throw an "InvalidStateError" DOMException. + + // 7. If index’s name is equal to name, terminate these steps. + if (index->name() == name) + return {}; + + // 8. If an index named name already exists in index’s object store, throw a "ConstraintError" DOMException. + for (auto const& existing_index : m_object_store_handle->index_set()) { + if (existing_index->name() == name) + return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_string); + } + + // 9. Set index’s name to name. + index->set_name(name); + + // 10. Set this’s name to name. + m_name = name; + + return {}; +} + +// https://w3c.github.io/IndexedDB/#dom-idbindex-keypath +JS::Value IDBIndex::key_path() const +{ + return m_index->key_path().visit( + [&](String const& value) -> JS::Value { + return JS::PrimitiveString::create(realm().vm(), value); + }, + [&](Vector const& value) -> JS::Value { + return JS::Array::create_from(realm(), value.span(), [&](auto const& entry) -> JS::Value { + return JS::PrimitiveString::create(realm().vm(), entry); + }); + }); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.h b/Libraries/LibWeb/IndexedDB/IDBIndex.h index 3e1b080a6db..ba8ef9f4815 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.h +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.h @@ -8,6 +8,8 @@ #include #include +#include +#include namespace Web::IndexedDB { @@ -18,11 +20,32 @@ class IDBIndex : public Bindings::PlatformObject { public: virtual ~IDBIndex() override; - [[nodiscard]] static GC::Ref create(JS::Realm&); + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, GC::Ref); + + WebIDL::ExceptionOr set_name(String const& value); + String name() const { return m_name; } + GC::Ref object_store() { return m_object_store_handle; } + JS::Value key_path() const; + bool multi_entry() const { return m_index->multi_entry(); } + bool unique() const { return m_index->unique(); } + + // The transaction of an index handle is the transaction of its associated object store handle. + GC::Ref transaction() { return m_object_store_handle->transaction(); } + GC::Ref index() { return m_index; } + GC::Ref store() { return m_object_store_handle; } protected: - explicit IDBIndex(JS::Realm&); + explicit IDBIndex(JS::Realm&, GC::Ref, GC::Ref); virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor& visitor) override; + +private: + // An index handle has an associated index and an associated object store handle. + GC::Ref m_index; + GC::Ref m_object_store_handle; + + // An index handle has a name, which is initialized to the name of the associated index when the index handle is created. + String m_name; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.idl b/Libraries/LibWeb/IndexedDB/IDBIndex.idl index a096122f40d..85e0ae37745 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.idl +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.idl @@ -2,11 +2,11 @@ [Exposed=(Window,Worker)] interface IDBIndex { - [FIXME] attribute DOMString name; - [FIXME, SameObject] readonly attribute IDBObjectStore objectStore; - [FIXME] readonly attribute any keyPath; - [FIXME] readonly attribute boolean multiEntry; - [FIXME] readonly attribute boolean unique; + attribute DOMString name; + [SameObject] readonly attribute IDBObjectStore objectStore; + readonly attribute any keyPath; + readonly attribute boolean multiEntry; + readonly attribute boolean unique; [FIXME, NewObject] IDBRequest get(any query); [FIXME, NewObject] IDBRequest getKey(any query); [FIXME, NewObject] IDBRequest getAll(optional any query, optional [EnforceRange] unsigned long count); From 3367352991e1d24c52aff91de2f24f961756ffd7 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 18:37:23 +0200 Subject: [PATCH 16/83] LibWeb/IDB: Implement IDBObjectStore::createIndex --- Libraries/LibWeb/IndexedDB/IDBIndex.cpp | 10 ++-- Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp | 52 +++++++++++++++++++ Libraries/LibWeb/IndexedDB/IDBObjectStore.h | 18 +++++-- Libraries/LibWeb/IndexedDB/IDBObjectStore.idl | 2 +- Libraries/LibWeb/IndexedDB/Internal/Index.cpp | 11 +++- Libraries/LibWeb/IndexedDB/Internal/Index.h | 2 +- .../LibWeb/IndexedDB/Internal/ObjectStore.h | 7 ++- 7 files changed, 88 insertions(+), 14 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.cpp b/Libraries/LibWeb/IndexedDB/IDBIndex.cpp index a62ea6e5e31..86cb1b9be33 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.cpp @@ -71,14 +71,16 @@ WebIDL::ExceptionOr IDBIndex::set_name(String const& value) return {}; // 8. If an index named name already exists in index’s object store, throw a "ConstraintError" DOMException. - for (auto const& existing_index : m_object_store_handle->index_set()) { - if (existing_index->name() == name) - return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_string); - } + if (index->object_store()->index_set().contains(name)) + return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_string); // 9. Set index’s name to name. index->set_name(name); + // NOTE: Update the key in the map so it still matches the name + auto old_value = m_object_store_handle->index_set().take(m_name).release_value(); + m_object_store_handle->index_set().set(name, old_value); + // 10. Set this’s name to name. m_name = name; diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp index fbbcbe1c205..35f7ea24f46 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Web::IndexedDB { @@ -41,6 +42,7 @@ void IDBObjectStore::visit_edges(Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_store); visitor.visit(m_transaction); + visitor.visit(m_indexes); } // https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath @@ -101,4 +103,54 @@ WebIDL::ExceptionOr IDBObjectStore::set_name(String const& value) return {}; } +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-createindex +WebIDL::ExceptionOr> IDBObjectStore::create_index(String const& name, KeyPath key_path, IDBIndexParameters options) +{ + auto& realm = this->realm(); + + // 1. Let transaction be this's transaction. + auto transaction = this->transaction(); + + // 2. Let store be this's object store. + auto store = this->store(); + + // 3. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException. + if (transaction->mode() != Bindings::IDBTransactionMode::Versionchange) + return WebIDL::InvalidStateError::create(realm, "Transaction is not an upgrade transaction"_string); + + // FIXME: 4. If store has been deleted, throw an "InvalidStateError" DOMException. + + // 5. If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException. + if (transaction->state() != IDBTransaction::TransactionState::Active) + return WebIDL::TransactionInactiveError::create(realm, "Transaction is not active while creating index"_string); + + // 6. If an index named name already exists in store, throw a "ConstraintError" DOMException. + if (store->index_set().contains(name)) + return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_string); + + // 7. If keyPath is not a valid key path, throw a "SyntaxError" DOMException. + if (!is_valid_key_path(key_path)) + return WebIDL::SyntaxError::create(realm, "Key path is not valid"_string); + + // 8. Let unique be options’s unique member. + auto unique = options.unique; + + // 9. Let multiEntry be options’s multiEntry member. + auto multi_entry = options.multi_entry; + + // 10. If keyPath is a sequence and multiEntry is true, throw an "InvalidAccessError" DOMException. + if (key_path.has>() && multi_entry) + return WebIDL::InvalidAccessError::create(realm, "Key path is a sequence and multiEntry is true"_string); + + // 11. Let index be a new index in store. + // Set index’s name to name, key path to keyPath, unique flag to unique, and multiEntry flag to multiEntry. + auto index = Index::create(realm, store, name, key_path, unique, multi_entry); + + // 12. Add index to this's index set. + this->index_set().set(name, index); + + // 13. Return a new index handle associated with index and this. + return IDBIndex::create(realm, index, *this); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h index c22fd5081ee..df6e0f03efb 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -13,6 +14,11 @@ namespace Web::IndexedDB { +struct IDBIndexParameters { + bool unique { false }; + bool multi_entry { false }; +}; + // https://w3c.github.io/IndexedDB/#object-store-interface // https://w3c.github.io/IndexedDB/#object-store-handle-construct class IDBObjectStore : public Bindings::PlatformObject { @@ -23,14 +29,17 @@ public: virtual ~IDBObjectStore() override; [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, GC::Ref); - JS::Value key_path() const; - GC::Ref transaction() const { return m_transaction; } - // https://w3c.github.io/IndexedDB/#dom-idbobjectstore-autoincrement // The autoIncrement getter steps are to return true if this’s object store has a key generator, and false otherwise. bool auto_increment() const { return m_store->key_generator().has_value(); } + JS::Value key_path() const; String name() const { return m_name; } WebIDL::ExceptionOr set_name(String const& value); + GC::Ref transaction() const { return m_transaction; } + GC::Ref store() const { return m_store; } + AK::HashMap>& index_set() { return m_indexes; } + + WebIDL::ExceptionOr> create_index(String const&, KeyPath, IDBIndexParameters options); protected: explicit IDBObjectStore(JS::Realm&, GC::Ref, GC::Ref); @@ -44,6 +53,9 @@ private: // An object store handle has a name, which is initialized to the name of the associated object store when the object store handle is created. String m_name; + + // An object store handle has an index set + AK::HashMap> m_indexes; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl index 66dd3ef722d..4af455b4102 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl @@ -22,7 +22,7 @@ interface IDBObjectStore { [FIXME, NewObject] IDBRequest openCursor(optional any query, optional IDBCursorDirection direction = "next"); [FIXME, NewObject] IDBRequest openKeyCursor(optional any query, optional IDBCursorDirection direction = "next"); [FIXME] IDBIndex index(DOMString name); - [FIXME, NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence) keyPath, optional IDBIndexParameters options = {}); + [NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence) keyPath, optional IDBIndexParameters options = {}); [FIXME] undefined deleteIndex(DOMString name); }; diff --git a/Libraries/LibWeb/IndexedDB/Internal/Index.cpp b/Libraries/LibWeb/IndexedDB/Internal/Index.cpp index 38ea6e1e27e..d347f1dd49f 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Index.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Index.cpp @@ -25,7 +25,7 @@ Index::Index(GC::Ref store, String name, KeyPath const& key_path, b , m_multi_entry(multi_entry) , m_key_path(key_path) { - store->add_index(*this); + store->index_set().set(name, *this); } void Index::visit_edges(Visitor& visitor) @@ -39,4 +39,13 @@ void Index::visit_edges(Visitor& visitor) } } +void Index::set_name(String name) +{ + // NOTE: Update the key in the map so it still matches the name + auto old_value = m_object_store->index_set().take(m_name).release_value(); + m_object_store->index_set().set(name, old_value); + + m_name = move(name); +} + } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Index.h b/Libraries/LibWeb/IndexedDB/Internal/Index.h index e984ffbc1b4..6296c8b805c 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Index.h +++ b/Libraries/LibWeb/IndexedDB/Internal/Index.h @@ -31,7 +31,7 @@ public: [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, String, KeyPath const&, bool, bool); virtual ~Index(); - void set_name(String name) { m_name = move(name); } + void set_name(String name); [[nodiscard]] String name() const { return m_name; } [[nodiscard]] bool unique() const { return m_unique; } [[nodiscard]] bool multi_entry() const { return m_multi_entry; } diff --git a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h index d7f400e37bc..810c33eb8fb 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -37,12 +38,10 @@ public: bool uses_inline_keys() const { return m_key_path.has_value(); } bool uses_out_of_line_keys() const { return !m_key_path.has_value(); } Optional key_generator() const { return m_key_generator; } + AK::HashMap>& index_set() { return m_indexes; } GC::Ref database() const { return m_database; } - void add_index(GC::Ref index) { m_indexes.append(index); } - ReadonlySpan> index_set() const { return m_indexes; } - protected: virtual void visit_edges(Visitor&) override; @@ -53,7 +52,7 @@ private: GC::Ref m_database; // AD-HOC: An Index has referenced ObjectStores, we also need the reverse mapping - Vector> m_indexes; + AK::HashMap> m_indexes; // An object store has a name, which is a name. At any one time, the name is unique within the database to which it belongs. String m_name; From fba7ad6969530d05fbb443410a1a91c3c43392f1 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 18:38:55 +0200 Subject: [PATCH 17/83] LibWeb/IDB: Implement IDBObjectStore::indexNames --- Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp | 12 ++++++++++++ Libraries/LibWeb/IndexedDB/IDBObjectStore.h | 1 + Libraries/LibWeb/IndexedDB/IDBObjectStore.idl | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp index 35f7ea24f46..255026e319b 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp @@ -153,4 +153,16 @@ WebIDL::ExceptionOr> IDBObjectStore::create_index(String const return IDBIndex::create(realm, index, *this); } +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames +GC::Ref IDBObjectStore::index_names() +{ + // 1. Let names be a list of the names of the indexes in this's index set. + Vector names; + for (auto const& [name, index] : m_indexes) + names.append(name); + + // 2. Return the result (a DOMStringList) of creating a sorted name list with names. + return create_a_sorted_name_list(realm(), names); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h index df6e0f03efb..8baf4a1869c 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h @@ -40,6 +40,7 @@ public: AK::HashMap>& index_set() { return m_indexes; } WebIDL::ExceptionOr> create_index(String const&, KeyPath, IDBIndexParameters options); + [[nodiscard]] GC::Ref index_names(); protected: explicit IDBObjectStore(JS::Realm&, GC::Ref, GC::Ref); diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl index 4af455b4102..550f7d3c60f 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl @@ -6,7 +6,7 @@ interface IDBObjectStore { attribute DOMString name; readonly attribute any keyPath; - [FIXME] readonly attribute DOMStringList indexNames; + readonly attribute DOMStringList indexNames; [SameObject] readonly attribute IDBTransaction transaction; readonly attribute boolean autoIncrement; From fce936e05a3e89a1be99193013ac66e16b390dd7 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 18:43:29 +0200 Subject: [PATCH 18/83] LibWeb/IDB: Implement IDBObjectStore::index --- Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp | 24 +++++++++++++++++++ Libraries/LibWeb/IndexedDB/IDBObjectStore.h | 1 + Libraries/LibWeb/IndexedDB/IDBObjectStore.idl | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp index 255026e319b..4a5b4999236 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp @@ -165,4 +165,28 @@ GC::Ref IDBObjectStore::index_names() return create_a_sorted_name_list(realm(), names); } +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index +WebIDL::ExceptionOr> IDBObjectStore::index(String const& name) +{ + // 1. Let transaction be this’s transaction. + auto transaction = this->transaction(); + + // 2. Let store be this’s object store. + [[maybe_unused]] auto store = this->store(); + + // FIXME: 3. If store has been deleted, throw an "InvalidStateError" DOMException. + + // 4. If transaction’s state is finished, then throw an "InvalidStateError" DOMException. + if (transaction->state() == IDBTransaction::TransactionState::Finished) + return WebIDL::InvalidStateError::create(realm(), "Transaction is finished"_string); + + // 5. Let index be the index named name in this’s index set if one exists, or throw a "NotFoundError" DOMException otherwise. + auto index = m_indexes.get(name); + if (!index.has_value()) + return WebIDL::NotFoundError::create(realm(), "Index not found"_string); + + // 6. Return an index handle associated with index and this. + return IDBIndex::create(realm(), *index, *this); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h index 8baf4a1869c..0d3aac2a589 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h @@ -41,6 +41,7 @@ public: WebIDL::ExceptionOr> create_index(String const&, KeyPath, IDBIndexParameters options); [[nodiscard]] GC::Ref index_names(); + WebIDL::ExceptionOr> index(String const&); protected: explicit IDBObjectStore(JS::Realm&, GC::Ref, GC::Ref); diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl index 550f7d3c60f..947ecd6bcf5 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl @@ -21,7 +21,7 @@ interface IDBObjectStore { [FIXME, NewObject] IDBRequest count(optional any query); [FIXME, NewObject] IDBRequest openCursor(optional any query, optional IDBCursorDirection direction = "next"); [FIXME, NewObject] IDBRequest openKeyCursor(optional any query, optional IDBCursorDirection direction = "next"); - [FIXME] IDBIndex index(DOMString name); + IDBIndex index(DOMString name); [NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence) keyPath, optional IDBIndexParameters options = {}); [FIXME] undefined deleteIndex(DOMString name); }; From 718c805e952fd6fac14d44591659e7eda4be357a Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 18:57:59 +0200 Subject: [PATCH 19/83] LibWeb/IDB: Implement IDBObjectStore::deleteIndex --- Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp | 33 +++++++++++++++++++ Libraries/LibWeb/IndexedDB/IDBObjectStore.h | 1 + Libraries/LibWeb/IndexedDB/IDBObjectStore.idl | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp index 4a5b4999236..aed485032f7 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp @@ -189,4 +189,37 @@ WebIDL::ExceptionOr> IDBObjectStore::index(String const& name) return IDBIndex::create(realm(), *index, *this); } +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-deleteindex +WebIDL::ExceptionOr IDBObjectStore::delete_index(String const& name) +{ + // 1. Let transaction be this’s transaction. + auto transaction = this->transaction(); + + // 2. Let store be this’s object store. + auto store = this->store(); + + // 3. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException. + if (transaction->mode() != Bindings::IDBTransactionMode::Versionchange) + return WebIDL::InvalidStateError::create(realm(), "Transaction is not an upgrade transaction"_string); + + // FIXME: 4. If store has been deleted, throw an "InvalidStateError" DOMException. + + // 5. If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException. + if (transaction->state() != IDBTransaction::TransactionState::Active) + return WebIDL::TransactionInactiveError::create(realm(), "Transaction is not active"_string); + + // 6. Let index be the index named name in store if one exists, or throw a "NotFoundError" DOMException otherwise. + auto index = m_indexes.get(name); + if (!index.has_value()) + return WebIDL::NotFoundError::create(realm(), "Index not found"_string); + + // 7. Remove index from this’s index set. + m_indexes.remove(name); + + // 8. Destroy index. + store->index_set().remove(name); + + return {}; +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h index 0d3aac2a589..0fab61694be 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h @@ -42,6 +42,7 @@ public: WebIDL::ExceptionOr> create_index(String const&, KeyPath, IDBIndexParameters options); [[nodiscard]] GC::Ref index_names(); WebIDL::ExceptionOr> index(String const&); + WebIDL::ExceptionOr delete_index(String const&); protected: explicit IDBObjectStore(JS::Realm&, GC::Ref, GC::Ref); diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl index 947ecd6bcf5..90d51d3d686 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl @@ -23,7 +23,7 @@ interface IDBObjectStore { [FIXME, NewObject] IDBRequest openKeyCursor(optional any query, optional IDBCursorDirection direction = "next"); IDBIndex index(DOMString name); [NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence) keyPath, optional IDBIndexParameters options = {}); - [FIXME] undefined deleteIndex(DOMString name); + undefined deleteIndex(DOMString name); }; dictionary IDBIndexParameters { From 5298ecfc94207beccb771e9d08ecd773644dc0e4 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Tue, 1 Apr 2025 19:51:22 +0200 Subject: [PATCH 20/83] LibWeb/IDB: Implement IDBTransaction attributes This also uncovered a bug, where the transactions type was never set :^) --- Libraries/LibWeb/IndexedDB/IDBRequest.cpp | 1 + Libraries/LibWeb/IndexedDB/IDBRequest.h | 2 +- Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 28 ++++++++++++--- Libraries/LibWeb/IndexedDB/IDBTransaction.h | 34 +++++++++++++++++-- Libraries/LibWeb/IndexedDB/IDBTransaction.idl | 4 +-- .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 7 ++-- .../LibWeb/IndexedDB/Internal/Database.cpp | 2 ++ 7 files changed, 63 insertions(+), 15 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBRequest.cpp b/Libraries/LibWeb/IndexedDB/IDBRequest.cpp index c1d5b58f29b..a2d6f81c5a7 100644 --- a/Libraries/LibWeb/IndexedDB/IDBRequest.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBRequest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace Web::IndexedDB { diff --git a/Libraries/LibWeb/IndexedDB/IDBRequest.h b/Libraries/LibWeb/IndexedDB/IDBRequest.h index e3031a22ccb..796cd4c9dad 100644 --- a/Libraries/LibWeb/IndexedDB/IDBRequest.h +++ b/Libraries/LibWeb/IndexedDB/IDBRequest.h @@ -10,7 +10,7 @@ #include #include -#include +#include namespace Web::IndexedDB { diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index e58115f3edd..0a1d94dc966 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, stelar7 + * Copyright (c) 2024-2025, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ @@ -15,15 +15,18 @@ GC_DEFINE_ALLOCATOR(IDBTransaction); IDBTransaction::~IDBTransaction() = default; -IDBTransaction::IDBTransaction(JS::Realm& realm, GC::Ref database) +IDBTransaction::IDBTransaction(JS::Realm& realm, GC::Ref connection, Bindings::IDBTransactionMode mode, Bindings::IDBTransactionDurability durability, Vector> scopes) : EventTarget(realm) - , m_connection(database) + , m_connection(connection) + , m_mode(mode) + , m_durability(durability) + , m_scope(move(scopes)) { } -GC::Ref IDBTransaction::create(JS::Realm& realm, GC::Ref database) +GC::Ref IDBTransaction::create(JS::Realm& realm, GC::Ref connection, Bindings::IDBTransactionMode mode, Bindings::IDBTransactionDurability durability = Bindings::IDBTransactionDurability::Default, Vector> scopes = {}) { - return realm.create(realm, database); + return realm.create(realm, connection, mode, durability, move(scopes)); } void IDBTransaction::initialize(JS::Realm& realm) @@ -38,6 +41,8 @@ void IDBTransaction::visit_edges(Visitor& visitor) visitor.visit(m_connection); visitor.visit(m_error); visitor.visit(m_associated_request); + visitor.visit(m_scope); + visitor.visit(m_cleanup_event_loop); } void IDBTransaction::set_onabort(WebIDL::CallbackType* event_handler) @@ -70,6 +75,7 @@ WebIDL::CallbackType* IDBTransaction::onerror() return event_handler_attribute(HTML::EventNames::error); } +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-abort WebIDL::ExceptionOr IDBTransaction::abort() { // 1. If this's state is committing or finished, then throw an "InvalidStateError" DOMException. @@ -82,4 +88,16 @@ WebIDL::ExceptionOr IDBTransaction::abort() return {}; } +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstorenames +GC::Ref IDBTransaction::object_store_names() +{ + // 1. Let names be a list of the names of the object stores in this's scope. + Vector names; + for (auto const& object_store : this->scope()) + names.append(object_store->name()); + + // 2. Return the result (a DOMStringList) of creating a sorted name list with names. + return create_a_sorted_name_list(realm(), names); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index 14881a69dd0..a7b35fd75e2 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -1,17 +1,22 @@ /* - * Copyright (c) 2024, stelar7 + * Copyright (c) 2024-2025, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include #include #include #include #include #include +#include #include +#include +#include +#include namespace Web::IndexedDB { @@ -30,7 +35,7 @@ class IDBTransaction : public DOM::EventTarget { public: virtual ~IDBTransaction() override; - [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref); + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector>); [[nodiscard]] Bindings::IDBTransactionMode mode() const { return m_mode; } [[nodiscard]] TransactionState state() const { return m_state; } [[nodiscard]] GC::Ptr error() const { return m_error; } @@ -38,6 +43,8 @@ public: [[nodiscard]] Bindings::IDBTransactionDurability durability() const { return m_durability; } [[nodiscard]] GC::Ptr associated_request() const { return m_associated_request; } [[nodiscard]] bool aborted() const { return m_aborted; } + [[nodiscard]] GC::Ref object_store_names(); + [[nodiscard]] ReadonlySpan> scope() const { return m_scope; } void set_mode(Bindings::IDBTransactionMode mode) { m_mode = mode; } void set_state(TransactionState state) { m_state = state; } @@ -59,18 +66,39 @@ public: WebIDL::CallbackType* onerror(); protected: - explicit IDBTransaction(JS::Realm&, GC::Ref); + explicit IDBTransaction(JS::Realm&, GC::Ref, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector>); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Visitor& visitor) override; private: + // AD-HOC: The transaction has a connection GC::Ref m_connection; + + // A transaction has a mode that determines which types of interactions can be performed upon that transaction. Bindings::IDBTransactionMode m_mode; + + // A transaction has a durability hint. This is a hint to the user agent of whether to prioritize performance or durability when committing the transaction. Bindings::IDBTransactionDurability m_durability { Bindings::IDBTransactionDurability::Default }; + + // A transaction has a state TransactionState m_state; + + // A transaction has a error which is set if the transaction is aborted. GC::Ptr m_error; + // A transaction has an associated upgrade request GC::Ptr m_associated_request; + + // AD-HOC: We need to track abort state separately, since we cannot rely on only the error. bool m_aborted { false }; + + // A transaction has a scope which is a set of object stores that the transaction may interact with. + Vector> m_scope; + + // A transaction has a request list of pending requests which have been made against the transaction. + RequestList m_request_list; + + // A transaction optionally has a cleanup event loop which is an event loop. + GC::Ptr m_cleanup_event_loop; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl index bba6179e240..c343a328ab3 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl @@ -4,10 +4,10 @@ [Exposed=(Window,Worker)] interface IDBTransaction : EventTarget { - [FIXME] readonly attribute DOMStringList objectStoreNames; + readonly attribute DOMStringList objectStoreNames; readonly attribute IDBTransactionMode mode; readonly attribute IDBTransactionDurability durability; - [FIXME, SameObject] readonly attribute IDBDatabase db; + [SameObject, ImplementedAs=connection] readonly attribute IDBDatabase db; readonly attribute DOMException? error; [FIXME] IDBObjectStore objectStore(DOMString name); [FIXME] undefined commit(); diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 6708cae50c8..4aa8fc7457d 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, stelar7 + * Copyright (c) 2024-2025, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ @@ -312,9 +312,8 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refassociated_database(); // 2. Let transaction be a new upgrade transaction with connection used as connection. - auto transaction = IDBTransaction::create(realm, connection); - - // FIXME: 3. Set transaction’s scope to connection’s object store set. + // 3. Set transaction’s scope to connection’s object store set. + auto transaction = IDBTransaction::create(realm, connection, Bindings::IDBTransactionMode::Versionchange, Bindings::IDBTransactionDurability::Default, Vector> { connection->object_store_set() }); // 4. Set db’s upgrade transaction to transaction. db->set_upgrade_transaction(transaction); diff --git a/Libraries/LibWeb/IndexedDB/Internal/Database.cpp b/Libraries/LibWeb/IndexedDB/Internal/Database.cpp index e02d0e38c52..cf4f5fd945c 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Database.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Database.cpp @@ -4,8 +4,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include +#include namespace Web::IndexedDB { From 4084a127de0dc3213265ba89132fef0f0c501bd5 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 2 Apr 2025 10:28:34 +0200 Subject: [PATCH 21/83] LibWeb/IDB: Change reference to a GC::Ref in abort_a_transaction --- .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 22 +++++++++---------- .../LibWeb/IndexedDB/Internal/Algorithms.h | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 4aa8fc7457d..621ab2a2dab 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -447,10 +447,10 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage } // https://w3c.github.io/IndexedDB/#abort-a-transaction -void abort_a_transaction(IDBTransaction& transaction, GC::Ptr error) +void abort_a_transaction(GC::Ref transaction, GC::Ptr error) { // NOTE: This is not spec'ed anywhere, but we need to know IF the transaction was aborted. - transaction.set_aborted(true); + transaction->set_aborted(true); // FIXME: 1. All the changes made to the database by the transaction are reverted. // For upgrade transactions this includes changes to the set of object stores and indexes, as well as the change to the version. @@ -461,11 +461,11 @@ void abort_a_transaction(IDBTransaction& transaction, GC::Ptrset_state(IDBTransaction::TransactionState::Finished); // 4. If error is not null, set transaction’s error to error. if (error) - transaction.set_error(error); + transaction->set_error(error); // FIXME: 5. For each request of transaction’s request list, abort the steps to asynchronously execute a request for request, // set request’s processed flag to true, and queue a task to run these steps: @@ -475,23 +475,23 @@ void abort_a_transaction(IDBTransaction& transaction, GC::Ptrrealm().vm().heap(), [transaction]() { // 1. If transaction is an upgrade transaction, then set transaction’s connection's associated database's upgrade transaction to null. - if (transaction.is_upgrade_transaction()) - transaction.connection()->associated_database()->set_upgrade_transaction(nullptr); + if (transaction->is_upgrade_transaction()) + transaction->connection()->associated_database()->set_upgrade_transaction(nullptr); // 2. Fire an event named abort at transaction with its bubbles attribute initialized to true. - transaction.dispatch_event(DOM::Event::create(transaction.realm(), HTML::EventNames::abort, { .bubbles = true })); + transaction->dispatch_event(DOM::Event::create(transaction->realm(), HTML::EventNames::abort, { .bubbles = true })); // 3. If transaction is an upgrade transaction, then: - if (transaction.is_upgrade_transaction()) { + if (transaction->is_upgrade_transaction()) { // 1. Let request be the open request associated with transaction. - auto request = transaction.associated_request(); + auto request = transaction->associated_request(); // 2. Set request’s transaction to null. // NOTE: Clear the two-way binding. request->set_transaction(nullptr); - transaction.set_associated_request(nullptr); + transaction->set_associated_request(nullptr); // 3. Set request’s result to undefined. request->set_result(JS::js_undefined()); diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h index 74e5b3f022f..907c2d1924b 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h @@ -23,7 +23,7 @@ ErrorOr> convert_a_value_to_a_key(JS::Realm&, JS::Value, Vector upgrade_a_database(JS::Realm&, GC::Ref, u64, GC::Ref); WebIDL::ExceptionOr delete_a_database(JS::Realm&, StorageAPI::StorageKey, String, GC::Ref); -void abort_a_transaction(IDBTransaction&, GC::Ptr); +void abort_a_transaction(GC::Ref, GC::Ptr); JS::Value convert_a_key_to_a_value(JS::Realm&, GC::Ref); bool is_valid_key_path(KeyPath const&); GC::Ref create_a_sorted_name_list(JS::Realm&, Vector); From 6ec914c7f7abb50d53ac56e2ffc2c531185ebd83 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 2 Apr 2025 10:28:04 +0200 Subject: [PATCH 22/83] LibWeb/IDB: Add some debug output --- AK/Debug.h.in | 4 ++ Libraries/LibWeb/IndexedDB/IDBDatabase.cpp | 2 + Libraries/LibWeb/IndexedDB/IDBDatabase.h | 4 ++ Libraries/LibWeb/IndexedDB/IDBRequest.cpp | 2 + Libraries/LibWeb/IndexedDB/IDBRequest.h | 8 ++++ Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 2 + Libraries/LibWeb/IndexedDB/IDBTransaction.h | 4 ++ .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 48 +++++++++++++++++++ Meta/CMake/all_the_debug_macros.cmake | 1 + 9 files changed, 75 insertions(+) diff --git a/AK/Debug.h.in b/AK/Debug.h.in index ddb495e39fa..85c58a87efd 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -114,6 +114,10 @@ # cmakedefine01 ICO_DEBUG #endif +#ifndef IDB_DEBUG +# cmakedefine01 IDB_DEBUG +#endif + #ifndef IDL_DEBUG # cmakedefine01 IDL_DEBUG #endif diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp index 808fc11d4bd..99afae48d30 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,7 @@ IDBDatabase::IDBDatabase(JS::Realm& realm, Database& db) , m_name(db.name()) , m_associated_database(db) { + m_uuid = MUST(Crypto::generate_random_uuid()); db.associate(*this); m_object_store_set = Vector> { db.object_stores() }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.h b/Libraries/LibWeb/IndexedDB/IDBDatabase.h index 69fa58e3b57..e83e0ad142b 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.h +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.h @@ -46,6 +46,7 @@ public: void set_close_pending(bool close_pending) { m_close_pending = close_pending; } void set_state(ConnectionState state) { m_state = state; } + [[nodiscard]] String uuid() const { return m_uuid; } [[nodiscard]] String name() const { return m_name; } [[nodiscard]] u64 version() const { return m_version; } [[nodiscard]] bool close_pending() const { return m_close_pending; } @@ -95,6 +96,9 @@ private: // NOTE: There is an associated database in the spec, but there is no mention where it is assigned, nor where its from // So we stash the one we have when opening a connection. GC::Ref m_associated_database; + + // NOTE: Used for debug purposes + String m_uuid; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBRequest.cpp b/Libraries/LibWeb/IndexedDB/IDBRequest.cpp index a2d6f81c5a7..25bb6febff2 100644 --- a/Libraries/LibWeb/IndexedDB/IDBRequest.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBRequest.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -21,6 +22,7 @@ IDBRequest::IDBRequest(JS::Realm& realm, IDBRequestSource source) : EventTarget(realm) , m_source(source) { + m_uuid = MUST(Crypto::generate_random_uuid()); } void IDBRequest::initialize(JS::Realm& realm) diff --git a/Libraries/LibWeb/IndexedDB/IDBRequest.h b/Libraries/LibWeb/IndexedDB/IDBRequest.h index 796cd4c9dad..a1c6509952f 100644 --- a/Libraries/LibWeb/IndexedDB/IDBRequest.h +++ b/Libraries/LibWeb/IndexedDB/IDBRequest.h @@ -30,6 +30,7 @@ public: [[nodiscard]] bool processed() const { return m_processed; } [[nodiscard]] IDBRequestSource source() const { return m_source; } [[nodiscard]] GC::Ptr transaction() const { return m_transaction; } + [[nodiscard]] String uuid() const { return m_uuid; } [[nodiscard]] Bindings::IDBRequestReadyState ready_state() const; [[nodiscard]] WebIDL::ExceptionOr> error() const; @@ -56,15 +57,22 @@ protected: private: // A request has a processed flag which is initially false. bool m_processed { false }; + // A request has a done flag which is initially false. bool m_done { false }; + // A request has a result and an error JS::Value m_result; GC::Ptr m_error; + // A request has a source object. IDBRequestSource m_source; + // A request has a transaction which is initially null. GC::Ptr m_transaction; + + // NOTE: Used for debug purposes + String m_uuid; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index 0a1d94dc966..a1d56e322cf 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -22,6 +23,7 @@ IDBTransaction::IDBTransaction(JS::Realm& realm, GC::Ref connection , m_durability(durability) , m_scope(move(scopes)) { + m_uuid = MUST(Crypto::generate_random_uuid()); } GC::Ref IDBTransaction::create(JS::Realm& realm, GC::Ref connection, Bindings::IDBTransactionMode mode, Bindings::IDBTransactionDurability durability = Bindings::IDBTransactionDurability::Default, Vector> scopes = {}) diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index a7b35fd75e2..5ca6ee15325 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -45,6 +45,7 @@ public: [[nodiscard]] bool aborted() const { return m_aborted; } [[nodiscard]] GC::Ref object_store_names(); [[nodiscard]] ReadonlySpan> scope() const { return m_scope; } + [[nodiscard]] String uuid() const { return m_uuid; } void set_mode(Bindings::IDBTransactionMode mode) { m_mode = mode; } void set_state(TransactionState state) { m_state = state; } @@ -100,5 +101,8 @@ private: // A transaction optionally has a cleanup event loop which is an event loop. GC::Ptr m_cleanup_event_loop; + + // NOTE: Used for debug purposes + String m_uuid; }; } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 621ab2a2dab..9dc3b7b72d6 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -36,9 +36,18 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 2. Add request to queue. queue.append(request); + dbgln_if(IDB_DEBUG, "open_a_database_connection: added request {} to queue", request->uuid()); // 3. Wait until all previous requests in queue have been processed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [queue, request]() { + if constexpr (IDB_DEBUG) { + dbgln("open_a_database_connection: waiting for step 3"); + dbgln("requests in queue:"); + for (auto const& item : queue) { + dbgln("[{}] - {} = {}", item == request ? "x"sv : " "sv, item->uuid(), item->processed() ? "processed"sv : "not processed"sv); + } + } + return queue.all_previous_requests_processed(request); })); @@ -71,6 +80,7 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 8. Let connection be a new connection to db. auto connection = IDBDatabase::create(realm, *db); + dbgln_if(IDB_DEBUG, "Created new connection with UUID: {}", connection->uuid()); // 9. Set connection’s version to version. connection->set_version(version); @@ -97,6 +107,11 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 3. Wait for all of the events to be fired. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [&events_to_fire, &events_fired]() { + if constexpr (IDB_DEBUG) { + dbgln("open_a_database_connection: waiting for step 10.3"); + dbgln("events_fired: {}, events_to_fire: {}", events_fired, events_to_fire); + } + return events_fired == events_to_fire; })); @@ -112,6 +127,14 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 5. Wait until all connections in openConnections are closed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [open_connections]() { + if constexpr (IDB_DEBUG) { + dbgln("open_a_database_connection: waiting for step 10.5"); + dbgln("open connections: {}", open_connections.size()); + for (auto const& connection : open_connections) { + dbgln(" - {}", connection->uuid()); + } + } + for (auto const& entry : open_connections) { if (entry->state() != IDBDatabase::ConnectionState::Closed) { return false; @@ -314,6 +337,7 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Ref> { connection->object_store_set() }); + dbgln_if(IDB_DEBUG, "Created new upgrade transaction with UUID: {}", transaction->uuid()); // 4. Set db’s upgrade transaction to transaction. db->set_upgrade_transaction(transaction); @@ -364,6 +388,7 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Ref delete_a_database(JS::Realm& realm, StorageAPI::Storage // 2. Add request to queue. queue.append(request); + dbgln_if(IDB_DEBUG, "delete_a_database: added request {} to queue", request->uuid()); // 3. Wait until all previous requests in queue have been processed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [queue, request]() { + if constexpr (IDB_DEBUG) { + dbgln("delete_a_database: waiting for step 3"); + dbgln("requests in queue:"); + for (auto const& item : queue) { + dbgln("[{}] - {} = {}", item == request ? "x"sv : " "sv, item->uuid(), item->processed() ? "processed"sv : "not processed"sv); + } + } + return queue.all_previous_requests_processed(request); })); @@ -411,6 +445,11 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage // 7. Wait for all of the events to be fired. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [&events_to_fire, &events_fired]() { + if constexpr (IDB_DEBUG) { + dbgln("delete_a_database: waiting for step 7"); + dbgln("events_fired: {}, events_to_fire: {}", events_fired, events_to_fire); + } + return events_fired == events_to_fire; })); @@ -425,6 +464,14 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage // 9. Wait until all connections in openConnections are closed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [open_connections]() { + if constexpr (IDB_DEBUG) { + dbgln("delete_a_database: waiting for step 9"); + dbgln("open connections: {}", open_connections.size()); + for (auto const& connection : open_connections) { + dbgln(" - {}", connection->uuid()); + } + } + for (auto const& entry : open_connections) { if (entry->state() != IDBDatabase::ConnectionState::Closed) { return false; @@ -451,6 +498,7 @@ void abort_a_transaction(GC::Ref transaction, GC::Ptrset_aborted(true); + dbgln_if(IDB_DEBUG, "abort_a_transaction: transaction {} is aborting", transaction->uuid()); // FIXME: 1. All the changes made to the database by the transaction are reverted. // For upgrade transactions this includes changes to the set of object stores and indexes, as well as the change to the version. diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 47d2db25da5..7e947c28df9 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -24,6 +24,7 @@ set(HTML_SCRIPT_DEBUG ON) set(HTTPJOB_DEBUG ON) set(HUNKS_DEBUG ON) set(ICO_DEBUG ON) +set(IDB_DEBUG ON) set(IDL_DEBUG ON) set(IMAGE_DECODER_DEBUG ON) set(IMAGE_LOADER_DEBUG ON) From 1fc2d6f1af128e529eff23c2a7038b42381d56e6 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 2 Apr 2025 11:14:28 +0200 Subject: [PATCH 23/83] LibWeb/IDB: Comment out infinite loop while waiting for next PR Since the steps needed to avoid this loop is quite long, adding this as a fixme for the future will make this PR easier to review --- Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 9dc3b7b72d6..6b1f81c648d 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -545,7 +545,7 @@ void abort_a_transaction(GC::Ref transaction, GC::Ptrset_result(JS::js_undefined()); // 4. Set request’s processed flag to false. - request->set_processed(false); + // FIXME: request->set_processed(false); // 5. Set request’s done flag to false. request->set_done(false); From f1fba245389ababa463dd5b1936cc14cccd24318 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 2 Apr 2025 11:14:50 +0200 Subject: [PATCH 24/83] LibWeb/IDB: Add ObjectStore to IDBDatabases store set --- Libraries/LibWeb/IndexedDB/IDBDatabase.cpp | 3 +++ Libraries/LibWeb/IndexedDB/IDBDatabase.h | 1 + 2 files changed, 4 insertions(+) diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp index 99afae48d30..781136c3b35 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp @@ -136,6 +136,9 @@ WebIDL::ExceptionOr> IDBDatabase::create_object_store(St // If keyPath is not null, set the created object store's key path to keyPath. auto object_store = ObjectStore::create(realm, database, name, auto_increment, key_path); + // AD-HOC: Add newly created object store to this's object store set. + add_to_object_store_set(object_store); + // 10. Return a new object store handle associated with store and transaction. return IDBObjectStore::create(realm, object_store, *transaction); } diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.h b/Libraries/LibWeb/IndexedDB/IDBDatabase.h index e83e0ad142b..d2127fcfc97 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.h +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.h @@ -53,6 +53,7 @@ public: [[nodiscard]] ConnectionState state() const { return m_state; } [[nodiscard]] GC::Ref associated_database() { return m_associated_database; } [[nodiscard]] ReadonlySpan> object_store_set() { return m_object_store_set; } + void add_to_object_store_set(GC::Ref object_store) { m_object_store_set.append(object_store); } void remove_from_object_store_set(GC::Ref object_store) { m_object_store_set.remove_first_matching([&](auto& entry) { return entry == object_store; }); From 938b1e91fec6f3345f75d77f0bf4603fd827fa76 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 9 Apr 2025 12:29:53 +0200 Subject: [PATCH 25/83] LibJS: Inline the fast path of Value::to_i32() and simplify to_u32() The fast path of to_i32() can be neatly inlined everywhere, and we still have to_i32_slow_case() for non-trivial conversions. For to_u32(), it really can just be implemented as a static cast to i32! --- Libraries/LibJS/Runtime/Value.cpp | 36 ------------------- Libraries/LibJS/Runtime/ValueInlines.h | 15 ++++++++ .../LibWeb/Bindings/ImageConstructor.cpp | 1 + Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp | 1 + Libraries/LibWeb/Crypto/SubtleCrypto.cpp | 1 + Libraries/LibWeb/DOM/NodeIterator.cpp | 1 + Libraries/LibWeb/DOM/TreeWalker.cpp | 1 + Libraries/LibWeb/WebAssembly/WebAssembly.cpp | 1 + Tests/LibWasm/test-wasm.cpp | 1 + 9 files changed, 22 insertions(+), 36 deletions(-) diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index 555dab57828..a01d563e9a0 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -953,42 +953,6 @@ ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const return static_cast(int32bit); } -// 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 -ThrowCompletionOr Value::to_i32(VM& vm) const -{ - if (is_int32()) - return as_i32(); - return to_i32_slow_case(vm); -} - -// 7.1.7 ToUint32 ( argument ), https://tc39.es/ecma262/#sec-touint32 -ThrowCompletionOr Value::to_u32(VM& vm) const -{ - // OPTIMIZATION: If this value is encoded as a positive i32, return it directly. - if (is_int32() && as_i32() >= 0) - return as_i32(); - - // 1. Let number be ? ToNumber(argument). - double number = TRY(to_number(vm)).as_double(); - - // 2. If number is not finite or number is either +0𝔽 or -0𝔽, return +0𝔽. - if (!isfinite(number) || number == 0) - return 0; - - // 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))). - auto int_val = floor(fabs(number)); - if (signbit(number)) - int_val = -int_val; - - // 4. Let int32bit be int modulo 2^32. - auto int32bit = modulo(int_val, NumericLimits::max() + 1.0); - - // 5. Return 𝔽(int32bit). - // Cast to i64 here to ensure that the double --> u32 cast doesn't invoke undefined behavior - // Otherwise, negative numbers cause a UBSAN warning. - return static_cast(static_cast(int32bit)); -} - // 7.1.8 ToInt16 ( argument ), https://tc39.es/ecma262/#sec-toint16 ThrowCompletionOr Value::to_i16(VM& vm) const { diff --git a/Libraries/LibJS/Runtime/ValueInlines.h b/Libraries/LibJS/Runtime/ValueInlines.h index 94d96701596..effa7f75eca 100644 --- a/Libraries/LibJS/Runtime/ValueInlines.h +++ b/Libraries/LibJS/Runtime/ValueInlines.h @@ -48,4 +48,19 @@ inline ThrowCompletionOr Value::to_primitive(VM& vm, PreferredType prefer return to_primitive_slow_case(vm, preferred_type); } +// 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 +inline ThrowCompletionOr Value::to_i32(VM& vm) const +{ + if (is_int32()) + return as_i32(); + + return to_i32_slow_case(vm); +} + +// 7.1.7 ToUint32 ( argument ), https://tc39.es/ecma262/#sec-touint32 +inline ThrowCompletionOr Value::to_u32(VM& vm) const +{ + return static_cast(TRY(to_i32(vm))); +} + } diff --git a/Libraries/LibWeb/Bindings/ImageConstructor.cpp b/Libraries/LibWeb/Bindings/ImageConstructor.cpp index 91449b8435c..8111de81942 100644 --- a/Libraries/LibWeb/Bindings/ImageConstructor.cpp +++ b/Libraries/LibWeb/Bindings/ImageConstructor.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp b/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp index 9ac38d71952..a97eff14290 100644 --- a/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp +++ b/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include diff --git a/Libraries/LibWeb/Crypto/SubtleCrypto.cpp b/Libraries/LibWeb/Crypto/SubtleCrypto.cpp index a5ac5afb87f..4fb748f40ff 100644 --- a/Libraries/LibWeb/Crypto/SubtleCrypto.cpp +++ b/Libraries/LibWeb/Crypto/SubtleCrypto.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/Libraries/LibWeb/DOM/NodeIterator.cpp b/Libraries/LibWeb/DOM/NodeIterator.cpp index 61f32451afd..a4f8def926b 100644 --- a/Libraries/LibWeb/DOM/NodeIterator.cpp +++ b/Libraries/LibWeb/DOM/NodeIterator.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibWeb/DOM/TreeWalker.cpp b/Libraries/LibWeb/DOM/TreeWalker.cpp index e2c6961a447..aeea0ae6f4e 100644 --- a/Libraries/LibWeb/DOM/TreeWalker.cpp +++ b/Libraries/LibWeb/DOM/TreeWalker.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibWeb/WebAssembly/WebAssembly.cpp b/Libraries/LibWeb/WebAssembly/WebAssembly.cpp index 89613052c6b..50eb5262e39 100644 --- a/Libraries/LibWeb/WebAssembly/WebAssembly.cpp +++ b/Libraries/LibWeb/WebAssembly/WebAssembly.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include diff --git a/Tests/LibWasm/test-wasm.cpp b/Tests/LibWasm/test-wasm.cpp index 037b56c3b63..39171d6d005 100644 --- a/Tests/LibWasm/test-wasm.cpp +++ b/Tests/LibWasm/test-wasm.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include From 8c8023465b494bec1cec8bc08452e9bf4b4f7670 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 9 Apr 2025 12:33:38 +0200 Subject: [PATCH 26/83] LibJS: Make use of arm64 FJCVTZS instruction if available FJCVTZS (Floating-point Javascript Convert to Signed fixed-point, rounding toward Zero) does exactly what we need for ToInt32 in the JavaScript specification. This isn't world-changing, but it does give a ~2% boost on compute- heavy benchmarks like JetStream, so we should obviously use it. --- Libraries/LibJS/Runtime/Value.cpp | 4 ++++ Libraries/LibJS/Runtime/ValueInlines.h | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index a01d563e9a0..f6660045812 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -934,6 +934,9 @@ ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const // 1. Let number be ? ToNumber(argument). double number = TRY(to_number(vm)).as_double(); +#if __has_builtin(__builtin_arm_jcvt) + return __builtin_arm_jcvt(number); +#else // 2. If number is not finite or number is either +0𝔽 or -0𝔽, return +0𝔽. if (!isfinite(number) || number == 0) return 0; @@ -951,6 +954,7 @@ ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const if (int32bit >= 2147483648.0) int32bit -= 4294967296.0; return static_cast(int32bit); +#endif } // 7.1.8 ToInt16 ( argument ), https://tc39.es/ecma262/#sec-toint16 diff --git a/Libraries/LibJS/Runtime/ValueInlines.h b/Libraries/LibJS/Runtime/ValueInlines.h index effa7f75eca..c9616e3843b 100644 --- a/Libraries/LibJS/Runtime/ValueInlines.h +++ b/Libraries/LibJS/Runtime/ValueInlines.h @@ -54,6 +54,11 @@ inline ThrowCompletionOr Value::to_i32(VM& vm) const if (is_int32()) return as_i32(); +#if __has_builtin(__builtin_arm_jcvt) + if (is_double()) + return __builtin_arm_jcvt(m_value.as_double); +#endif + return to_i32_slow_case(vm); } From fc111537bb51f805eedb5cde2ff9cc5f1126148d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 9 Apr 2025 23:08:32 +0200 Subject: [PATCH 27/83] LibJS: Move Value::to_i32() and to_u32() back out-of-line While good on arm64, this appears to have angered the x86_64 benchmark runner, so let's just put them back out-of-line. --- Libraries/LibJS/Runtime/Value.cpp | 20 ++++++++++++++++++++ Libraries/LibJS/Runtime/ValueInlines.h | 20 -------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index f6660045812..a61ef9c9bef 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -926,6 +926,26 @@ ThrowCompletionOr Value::to_property_key(VM& vm) const return MUST(key.to_string(vm)); } +// 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 +ThrowCompletionOr Value::to_i32(VM& vm) const +{ + if (is_int32()) + return as_i32(); + +#if __has_builtin(__builtin_arm_jcvt) + if (is_double()) + return __builtin_arm_jcvt(m_value.as_double); +#endif + + return to_i32_slow_case(vm); +} + +// 7.1.7 ToUint32 ( argument ), https://tc39.es/ecma262/#sec-touint32 +ThrowCompletionOr Value::to_u32(VM& vm) const +{ + return static_cast(TRY(to_i32(vm))); +} + // 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const { diff --git a/Libraries/LibJS/Runtime/ValueInlines.h b/Libraries/LibJS/Runtime/ValueInlines.h index c9616e3843b..94d96701596 100644 --- a/Libraries/LibJS/Runtime/ValueInlines.h +++ b/Libraries/LibJS/Runtime/ValueInlines.h @@ -48,24 +48,4 @@ inline ThrowCompletionOr Value::to_primitive(VM& vm, PreferredType prefer return to_primitive_slow_case(vm, preferred_type); } -// 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 -inline ThrowCompletionOr Value::to_i32(VM& vm) const -{ - if (is_int32()) - return as_i32(); - -#if __has_builtin(__builtin_arm_jcvt) - if (is_double()) - return __builtin_arm_jcvt(m_value.as_double); -#endif - - return to_i32_slow_case(vm); -} - -// 7.1.7 ToUint32 ( argument ), https://tc39.es/ecma262/#sec-touint32 -inline ThrowCompletionOr Value::to_u32(VM& vm) const -{ - return static_cast(TRY(to_i32(vm))); -} - } From d6080d1fdc8dcb6a7ca1c9b9ead0bd69b9d60ede Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Wed, 9 Apr 2025 20:54:41 +0200 Subject: [PATCH 28/83] LibIPC: Change TransportSocket to write large messages in small chunks With this change TransportSocket becomes capable of sending large messages without relying on workarounds, such as sending the message as a shared memory file descriptor when it can't fully fit into the socket buffer. It's implemented by combining all enqueued messages into two buffers: one for bytes and another for fds, and repeatedly attempts to write them in smaller chunks, waiting for the socket to become writable again if the receiver needs time to consume the data. Another significant improvement brought by this change is that we no longer drop messages queued for sending if the socket doesn't become writable after a 100ms timeout. Instead, we return the message to the send buffer and continue waiting for the socket to become writable. --- Libraries/LibIPC/TransportSocket.cpp | 119 ++++++++++++++++----------- Libraries/LibIPC/TransportSocket.h | 38 ++++++--- 2 files changed, 95 insertions(+), 62 deletions(-) diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 86239539ce7..762aacdb18a 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -13,26 +13,77 @@ namespace IPC { +void SendQueue::enqueue_message(Vector&& bytes, Vector&& fds) +{ + Threading::MutexLocker locker(m_mutex); + m_bytes.append(bytes.data(), bytes.size()); + m_fds.append(fds.data(), fds.size()); + m_condition.signal(); +} + +SendQueue::Running SendQueue::block_until_message_enqueued() +{ + Threading::MutexLocker locker(m_mutex); + while (m_bytes.is_empty() && m_fds.is_empty() && m_running) + m_condition.wait(); + return m_running ? Running::Yes : Running::No; +} + +SendQueue::BytesAndFds SendQueue::dequeue(size_t max_bytes) +{ + Threading::MutexLocker locker(m_mutex); + auto bytes_to_send = min(max_bytes, m_bytes.size()); + Vector bytes; + bytes.append(m_bytes.data(), bytes_to_send); + m_bytes.remove(0, bytes_to_send); + return { move(bytes), move(m_fds) }; +} + +void SendQueue::return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds) +{ + Threading::MutexLocker locker(m_mutex); + m_bytes.prepend(bytes.data(), bytes.size()); + m_fds.prepend(fds.data(), fds.size()); +} + +void SendQueue::stop() +{ + Threading::MutexLocker locker(m_mutex); + m_running = false; + m_condition.signal(); +} + TransportSocket::TransportSocket(NonnullOwnPtr socket) : m_socket(move(socket)) { m_send_queue = adopt_ref(*new SendQueue); m_send_thread = Threading::Thread::construct([this, send_queue = m_send_queue]() -> intptr_t { for (;;) { - send_queue->mutex.lock(); - while (send_queue->messages.is_empty() && send_queue->running) - send_queue->condition.wait(); - - if (!send_queue->running) { - send_queue->mutex.unlock(); + if (send_queue->block_until_message_enqueued() == SendQueue::Running::No) break; + + auto [bytes, fds] = send_queue->dequeue(4096); + ReadonlyBytes bytes_to_send = bytes; + + auto result = send_message(*m_socket, bytes_to_send, fds); + if (result.is_error()) { + dbgln("TransportSocket::send_thread: {}", result.error()); + VERIFY_NOT_REACHED(); } - auto [bytes, fds] = send_queue->messages.take_first(); - send_queue->mutex.unlock(); + if (!bytes.is_empty() || !fds.is_empty()) { + send_queue->return_unsent_data_to_front_of_queue(bytes_to_send, fds); + } - if (auto result = send_message(*m_socket, bytes, fds); result.is_error()) { - dbgln("TransportSocket::send_thread: {}", result.error()); + { + Vector pollfds; + if (pollfds.is_empty()) + pollfds.append({ .fd = m_socket->fd().value(), .events = POLLOUT, .revents = 0 }); + + ErrorOr result { 0 }; + do { + result = Core::System::poll(pollfds, -1); + } while (result.is_error() && result.error().code() == EINTR); } } return 0; @@ -45,11 +96,7 @@ TransportSocket::TransportSocket(NonnullOwnPtr socket) TransportSocket::~TransportSocket() { - { - Threading::MutexLocker locker(m_send_queue->mutex); - m_send_queue->running = false; - m_send_queue->condition.signal(); - } + m_send_queue->stop(); (void)m_send_thread->join(); } @@ -114,55 +161,27 @@ void TransportSocket::post_message(Vector const& bytes_to_write, Vectorenqueue_message(move(message_buffer), move(raw_fds)); } -void TransportSocket::queue_message_on_send_thread(MessageToSend&& message_to_send) const -{ - Threading::MutexLocker lock(m_send_queue->mutex); - m_send_queue->messages.append(move(message_to_send)); - m_send_queue->condition.signal(); -} - -ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes&& bytes_to_write, Vector const& unowned_fds) +ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes& bytes_to_write, Vector& unowned_fds) { auto num_fds_to_transfer = unowned_fds.size(); while (!bytes_to_write.is_empty()) { ErrorOr maybe_nwritten = 0; if (num_fds_to_transfer > 0) { maybe_nwritten = socket.send_message(bytes_to_write, 0, unowned_fds); - if (!maybe_nwritten.is_error()) + if (!maybe_nwritten.is_error()) { num_fds_to_transfer = 0; + unowned_fds.clear(); + } } else { maybe_nwritten = socket.write_some(bytes_to_write); } if (maybe_nwritten.is_error()) { if (auto error = maybe_nwritten.release_error(); error.is_errno() && (error.code() == EAGAIN || error.code() == EWOULDBLOCK)) { - - // FIXME: Refactor this to pass the unwritten bytes back to the caller to send 'later' - // or next time the socket is writable - Vector pollfds; - if (pollfds.is_empty()) - pollfds.append({ .fd = socket.fd().value(), .events = POLLOUT, .revents = 0 }); - - ErrorOr result { 0 }; - do { - constexpr u32 POLL_TIMEOUT_MS = 100; - result = Core::System::poll(pollfds, POLL_TIMEOUT_MS); - } while (result.is_error() && result.error().code() == EINTR); - - if (!result.is_error() && result.value() != 0) - continue; - - switch (error.code()) { - case EPIPE: - return Error::from_string_literal("IPC::transfer_message: Disconnected from peer"); - case EAGAIN: - return Error::from_string_literal("IPC::transfer_message: Timed out waiting for socket to become writable"); - default: - return Error::from_syscall("IPC::transfer_message write"sv, -error.code()); - } + return {}; } else { return error; } @@ -252,7 +271,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib header.fd_count = received_fd_count; header.type = MessageHeader::Type::FileDescriptorAcknowledgement; memcpy(message_buffer.data(), &header, sizeof(MessageHeader)); - queue_message_on_send_thread({ move(message_buffer), {} }); + m_send_queue->enqueue_message(move(message_buffer), {}); } if (index < m_unprocessed_bytes.size()) { diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index 2c466d0f08c..8262cea5cc8 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -42,6 +42,31 @@ private: int m_fd; }; +class SendQueue : public AtomicRefCounted { +public: + enum class Running { + No, + Yes, + }; + Running block_until_message_enqueued(); + void stop(); + + void enqueue_message(Vector&& bytes, Vector&& fds); + struct BytesAndFds { + Vector bytes; + Vector fds; + }; + BytesAndFds dequeue(size_t max_bytes); + void return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds); + +private: + Vector m_bytes; + Vector m_fds; + Threading::Mutex m_mutex; + Threading::ConditionVariable m_condition { m_mutex }; + bool m_running { true }; +}; + class TransportSocket { AK_MAKE_NONCOPYABLE(TransportSocket); AK_MAKE_NONMOVABLE(TransportSocket); @@ -76,7 +101,7 @@ public: ErrorOr clone_for_transfer(); private: - static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes&&, Vector const& unowned_fds); + static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes& bytes, Vector& unowned_fds); NonnullOwnPtr m_socket; ByteBuffer m_unprocessed_bytes; @@ -87,19 +112,8 @@ private: // descriptor contained in the message before the peer receives it. https://openradar.me/9477351 Queue> m_fds_retained_until_received_by_peer; - struct MessageToSend { - Vector bytes; - Vector fds; - }; - struct SendQueue : public AtomicRefCounted { - AK::SinglyLinkedList messages; - Threading::Mutex mutex; - Threading::ConditionVariable condition { mutex }; - bool running { true }; - }; RefPtr m_send_thread; RefPtr m_send_queue; - void queue_message_on_send_thread(MessageToSend&&) const; }; } From 2d625f5c2343169d7e99f403225de47915859836 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Wed, 9 Apr 2025 22:24:08 +0200 Subject: [PATCH 29/83] LibIPC+LibWeb: Delete LargeMessageWrapper workaround in IPC connection It's no longer needed because TransportSocket is now capable of properly sending large messages. --- Libraries/LibIPC/Connection.cpp | 31 +++--------- Libraries/LibIPC/Connection.h | 10 ++-- Libraries/LibIPC/Decoder.h | 7 ++- Libraries/LibIPC/Message.cpp | 50 ------------------- Libraries/LibIPC/Message.h | 32 ------------ Libraries/LibIPC/TransportSocket.cpp | 4 +- Libraries/LibIPC/TransportSocket.h | 7 ++- Libraries/LibIPC/UnprocessedFileDescriptors.h | 36 ------------- Libraries/LibWeb/HTML/MessagePort.cpp | 10 ++-- Libraries/LibWeb/HTML/MessagePort.h | 1 - .../Tools/CodeGenerators/IPCCompiler/main.cpp | 12 ++--- 11 files changed, 23 insertions(+), 177 deletions(-) delete mode 100644 Libraries/LibIPC/UnprocessedFileDescriptors.h diff --git a/Libraries/LibIPC/Connection.cpp b/Libraries/LibIPC/Connection.cpp index cc02f30d85b..8f15685393d 100644 --- a/Libraries/LibIPC/Connection.cpp +++ b/Libraries/LibIPC/Connection.cpp @@ -12,7 +12,6 @@ #include #include #include -#include namespace IPC { @@ -40,21 +39,16 @@ bool ConnectionBase::is_open() const ErrorOr ConnectionBase::post_message(Message const& message) { - return post_message(message.endpoint_magic(), TRY(message.encode())); + return post_message(TRY(message.encode())); } -ErrorOr ConnectionBase::post_message(u32 endpoint_magic, MessageBuffer buffer) +ErrorOr ConnectionBase::post_message(MessageBuffer buffer) { // NOTE: If this connection is being shut down, but has not yet been destroyed, // the socket will be closed. Don't try to send more messages. if (!m_transport->is_open()) return Error::from_string_literal("Trying to post_message during IPC shutdown"); - if (buffer.data().size() > TransportSocket::SOCKET_BUFFER_SIZE) { - auto wrapper = LargeMessageWrapper::create(endpoint_magic, buffer); - buffer = MUST(wrapper->encode()); - } - MUST(buffer.transfer_message(*m_transport)); m_responsiveness_timer->start(); @@ -85,7 +79,7 @@ void ConnectionBase::handle_messages() } if (auto response = handler_result.release_value()) { - if (auto post_result = post_message(m_local_endpoint_magic, *response); post_result.is_error()) { + if (auto post_result = post_message(*response); post_result.is_error()) { dbgln("IPC::ConnectionBase::handle_messages: {}", post_result.error()); } } @@ -100,24 +94,11 @@ void ConnectionBase::wait_for_transport_to_become_readable() ErrorOr ConnectionBase::drain_messages_from_peer() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& unparsed_message) { - auto const& bytes = unparsed_message.bytes; - UnprocessedFileDescriptors unprocessed_fds; - unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); - if (auto message = try_parse_message(bytes, unprocessed_fds)) { - if (message->message_id() == LargeMessageWrapper::MESSAGE_ID) { - LargeMessageWrapper* wrapper = static_cast(message.ptr()); - auto wrapped_message = wrapper->wrapped_message_data(); - unprocessed_fds.return_fds_to_front_of_queue(wrapper->take_fds()); - auto parsed_message = try_parse_message(wrapped_message, unprocessed_fds); - VERIFY(parsed_message); - m_unprocessed_messages.append(parsed_message.release_nonnull()); - return; - } - + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& raw_message) { + if (auto message = try_parse_message(raw_message.bytes, raw_message.fds)) { m_unprocessed_messages.append(message.release_nonnull()); } else { - dbgln("Failed to parse IPC message {:hex-dump}", bytes); + dbgln("Failed to parse IPC message {:hex-dump}", raw_message.bytes); VERIFY_NOT_REACHED(); } }); diff --git a/Libraries/LibIPC/Connection.h b/Libraries/LibIPC/Connection.h index 0f5b918226f..c1644e66b6a 100644 --- a/Libraries/LibIPC/Connection.h +++ b/Libraries/LibIPC/Connection.h @@ -15,10 +15,6 @@ #include #include #include -#include -#include -#include -#include namespace IPC { @@ -30,7 +26,7 @@ public: [[nodiscard]] bool is_open() const; ErrorOr post_message(Message const&); - ErrorOr post_message(u32 endpoint_magic, MessageBuffer); + ErrorOr post_message(MessageBuffer); void shutdown(); virtual void die() { } @@ -43,7 +39,7 @@ protected: virtual void may_have_become_unresponsive() { } virtual void did_become_responsive() { } virtual void shutdown_with_error(Error const&); - virtual OwnPtr try_parse_message(ReadonlyBytes, UnprocessedFileDescriptors&) = 0; + virtual OwnPtr try_parse_message(ReadonlyBytes, Queue&) = 0; OwnPtr wait_for_specific_endpoint_message_impl(u32 endpoint_magic, int message_id); void wait_for_transport_to_become_readable(); @@ -102,7 +98,7 @@ protected: return {}; } - virtual OwnPtr try_parse_message(ReadonlyBytes bytes, UnprocessedFileDescriptors& fds) override + virtual OwnPtr try_parse_message(ReadonlyBytes bytes, Queue& fds) override { auto local_message = LocalEndpoint::decode_message(bytes, fds); if (!local_message.is_error()) diff --git a/Libraries/LibIPC/Decoder.h b/Libraries/LibIPC/Decoder.h index c7fdd892b72..7fa284c26a2 100644 --- a/Libraries/LibIPC/Decoder.h +++ b/Libraries/LibIPC/Decoder.h @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -38,7 +37,7 @@ inline ErrorOr decode(Decoder&) class Decoder { public: - Decoder(Stream& stream, UnprocessedFileDescriptors& files) + Decoder(Stream& stream, Queue& files) : m_stream(stream) , m_files(files) { @@ -63,11 +62,11 @@ public: ErrorOr decode_size(); Stream& stream() { return m_stream; } - UnprocessedFileDescriptors& files() { return m_files; } + Queue& files() { return m_files; } private: Stream& m_stream; - UnprocessedFileDescriptors& m_files; + Queue& m_files; }; template diff --git a/Libraries/LibIPC/Message.cpp b/Libraries/LibIPC/Message.cpp index 5652bac21cb..680cc329d08 100644 --- a/Libraries/LibIPC/Message.cpp +++ b/Libraries/LibIPC/Message.cpp @@ -6,7 +6,6 @@ #include #include -#include #include namespace IPC { @@ -47,53 +46,4 @@ ErrorOr MessageBuffer::transfer_message(Transport& transport) return {}; } -NonnullOwnPtr LargeMessageWrapper::create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap) -{ - auto size = buffer_to_wrap.data().size(); - auto wrapped_message_data = MUST(Core::AnonymousBuffer::create_with_size(size)); - memcpy(wrapped_message_data.data(), buffer_to_wrap.data().data(), size); - Vector files; - for (auto& owned_fd : buffer_to_wrap.take_fds()) { - files.append(File::adopt_fd(owned_fd->take_fd())); - } - return make(endpoint_magic, move(wrapped_message_data), move(files)); -} - -LargeMessageWrapper::LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds) - : m_endpoint_magic(endpoint_magic) - , m_wrapped_message_data(move(wrapped_message_data)) - , m_wrapped_fds(move(wrapped_fds)) -{ -} - -ErrorOr LargeMessageWrapper::encode() const -{ - MessageBuffer buffer; - Encoder stream { buffer }; - TRY(stream.encode(m_endpoint_magic)); - TRY(stream.encode(MESSAGE_ID)); - TRY(stream.encode(m_wrapped_message_data)); - TRY(stream.encode(m_wrapped_fds.size())); - for (auto const& wrapped_fd : m_wrapped_fds) { - TRY(stream.append_file_descriptor(wrapped_fd.take_fd())); - } - - return buffer; -} - -ErrorOr> LargeMessageWrapper::decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files) -{ - Decoder decoder { stream, files }; - auto wrapped_message_data = TRY(decoder.decode()); - - Vector wrapped_fds; - auto num_fds = TRY(decoder.decode()); - for (u32 i = 0; i < num_fds; ++i) { - auto fd = TRY(decoder.decode()); - wrapped_fds.append(move(fd)); - } - - return make(endpoint_magic, wrapped_message_data, move(wrapped_fds)); -} - } diff --git a/Libraries/LibIPC/Message.h b/Libraries/LibIPC/Message.h index 65333340f2a..e79638618ed 100644 --- a/Libraries/LibIPC/Message.h +++ b/Libraries/LibIPC/Message.h @@ -8,14 +8,8 @@ #pragma once #include -#include -#include #include -#include -#include -#include #include -#include namespace IPC { @@ -67,30 +61,4 @@ protected: Message() = default; }; -class LargeMessageWrapper : public Message { -public: - ~LargeMessageWrapper() override = default; - - static constexpr int MESSAGE_ID = 0x0; - - static NonnullOwnPtr create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap); - - u32 endpoint_magic() const override { return m_endpoint_magic; } - int message_id() const override { return MESSAGE_ID; } - char const* message_name() const override { return "LargeMessageWrapper"; } - ErrorOr encode() const override; - - static ErrorOr> decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files); - - ReadonlyBytes wrapped_message_data() const { return ReadonlyBytes { m_wrapped_message_data.data(), m_wrapped_message_data.size() }; } - auto take_fds() { return move(m_wrapped_fds); } - - LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds); - -private: - u32 m_endpoint_magic { 0 }; - Core::AnonymousBuffer m_wrapped_message_data; - Vector m_wrapped_fds; -}; - } diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 762aacdb18a..5800e165333 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -192,7 +192,7 @@ ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyB return {}; } -TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) +TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) { bool should_shutdown = false; while (is_open()) { @@ -241,7 +241,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib Message message; received_fd_count += header.fd_count; for (size_t i = 0; i < header.fd_count; ++i) - message.fds.append(m_unprocessed_fds.dequeue()); + message.fds.enqueue(m_unprocessed_fds.dequeue()); message.bytes.append(m_unprocessed_bytes.data() + index + sizeof(MessageHeader), header.payload_size); callback(move(message)); } else if (header.type == MessageHeader::Type::FileDescriptorAcknowledgement) { diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index 8262cea5cc8..1c3c2a64b0d 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -91,9 +90,9 @@ public: }; struct Message { Vector bytes; - Vector fds; + Queue fds; }; - ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&& schedule_shutdown); + ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&& schedule_shutdown); // Obnoxious name to make it clear that this is a dangerous operation. ErrorOr release_underlying_transport_for_transfer(); @@ -105,7 +104,7 @@ private: NonnullOwnPtr m_socket; ByteBuffer m_unprocessed_bytes; - UnprocessedFileDescriptors m_unprocessed_fds; + Queue m_unprocessed_fds; // After file descriptor is sent, it is moved to the wait queue until an acknowledgement is received from the peer. // This is necessary to handle a specific behavior of the macOS kernel, which may prematurely garbage-collect the file diff --git a/Libraries/LibIPC/UnprocessedFileDescriptors.h b/Libraries/LibIPC/UnprocessedFileDescriptors.h deleted file mode 100644 index 991c5598b8a..00000000000 --- a/Libraries/LibIPC/UnprocessedFileDescriptors.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2025, Aliaksandr Kalenik - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include - -namespace IPC { - -class UnprocessedFileDescriptors { -public: - void enqueue(File&& fd) - { - m_fds.append(move(fd)); - } - - File dequeue() - { - return m_fds.take_first(); - } - - void return_fds_to_front_of_queue(Vector&& fds) - { - m_fds.prepend(move(fds)); - } - - size_t size() const { return m_fds.size(); } - -private: - Vector m_fds; -}; - -} diff --git a/Libraries/LibWeb/HTML/MessagePort.cpp b/Libraries/LibWeb/HTML/MessagePort.cpp index 03fe434b6ec..97d6a808e80 100644 --- a/Libraries/LibWeb/HTML/MessagePort.cpp +++ b/Libraries/LibWeb/HTML/MessagePort.cpp @@ -288,13 +288,9 @@ void MessagePort::post_port_message(SerializedTransferRecord serialize_with_tran void MessagePort::read_from_transport() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& unparsed_message) { - auto& bytes = unparsed_message.bytes; - IPC::UnprocessedFileDescriptors unprocessed_fds; - unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); - - FixedMemoryStream stream { bytes.span(), FixedMemoryStream::Mode::ReadOnly }; - IPC::Decoder decoder { stream, unprocessed_fds }; + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& raw_message) { + FixedMemoryStream stream { raw_message.bytes.span(), FixedMemoryStream::Mode::ReadOnly }; + IPC::Decoder decoder { stream, raw_message.fds }; auto serialized_transfer_record = MUST(decoder.decode()); diff --git a/Libraries/LibWeb/HTML/MessagePort.h b/Libraries/LibWeb/HTML/MessagePort.h index 0d48fd0adfb..24e3bbeccbf 100644 --- a/Libraries/LibWeb/HTML/MessagePort.h +++ b/Libraries/LibWeb/HTML/MessagePort.h @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp index 4d041620171..c7fe62dfa4a 100644 --- a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp @@ -404,7 +404,7 @@ public:)~~~"); static i32 static_message_id() { return (int)MessageID::@message.pascal_name@; } virtual const char* message_name() const override { return "@endpoint.name@::@message.pascal_name@"; } - static ErrorOr> decode(Stream& stream, IPC::UnprocessedFileDescriptors& files) + static ErrorOr> decode(Stream& stream, Queue& files) { IPC::Decoder decoder { stream, files };)~~~"); @@ -649,7 +649,7 @@ void generate_proxy_method(SourceGenerator& message_generator, Endpoint const& e } } else { message_generator.append(R"~~~()); - MUST(m_connection.post_message(@endpoint.magic@, move(message_buffer))); )~~~"); + MUST(m_connection.post_message(move(message_buffer))); )~~~"); } message_generator.appendln(R"~~~( @@ -720,7 +720,7 @@ public: static u32 static_magic() { return @endpoint.magic@; } - static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] IPC::UnprocessedFileDescriptors& files) + static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] Queue& files) { FixedMemoryStream stream { buffer }; auto message_endpoint_magic = TRY(stream.read_value());)~~~"); @@ -757,11 +757,6 @@ public: do_decode_message(message.response_name()); } - generator.append(R"~~~( - case (int)IPC::LargeMessageWrapper::MESSAGE_ID: - return TRY(IPC::LargeMessageWrapper::decode(message_endpoint_magic, stream, files)); -)~~~"); - generator.append(R"~~~( default:)~~~"); if constexpr (GENERATE_DEBUG) { @@ -903,7 +898,6 @@ void build(StringBuilder& builder, Vector const& endpoints) #include #include #include -#include #if defined(AK_COMPILER_CLANG) #pragma clang diagnostic push From 6bd2cf319510d588c986d4b39ad46d69c7f62412 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 9 Apr 2025 20:57:35 +0200 Subject: [PATCH 30/83] LibWeb: Make Document::m_shadow_roots an IntrusiveList This makes unregistering a ShadowRoot O(1) instead of O(n) and erases a 2.2% item entirely from the Speedometer 2.1 profile. --- Libraries/LibWeb/DOM/Document.cpp | 9 ++++----- Libraries/LibWeb/DOM/Document.h | 3 ++- Libraries/LibWeb/DOM/ShadowRoot.h | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 759c4dbd891..a159978c1f7 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -584,7 +584,8 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_adopted_style_sheets); - visitor.visit(m_shadow_roots); + for (auto& shadow_root : m_shadow_roots) + visitor.visit(shadow_root); visitor.visit(m_top_layer_elements); visitor.visit(m_top_layer_pending_removals); @@ -5953,9 +5954,7 @@ void Document::register_shadow_root(Badge, DOM::ShadowRoot& sha void Document::unregister_shadow_root(Badge, DOM::ShadowRoot& shadow_root) { - m_shadow_roots.remove_all_matching([&](auto& item) { - return item.ptr() == &shadow_root; - }); + m_shadow_roots.remove(shadow_root); } void Document::for_each_shadow_root(Function&& callback) @@ -5967,7 +5966,7 @@ void Document::for_each_shadow_root(Function&& callback) void Document::for_each_shadow_root(Function&& callback) const { for (auto& shadow_root : m_shadow_roots) - callback(shadow_root); + callback(const_cast(shadow_root)); } bool Document::is_decoded_svg() const diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index edd6c9c863a..4a11faaef7a 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -1180,7 +1181,7 @@ private: mutable GC::Ptr m_adopted_style_sheets; - Vector> m_shadow_roots; + ShadowRoot::DocumentShadowRootList m_shadow_roots; Optional m_last_modified; diff --git a/Libraries/LibWeb/DOM/ShadowRoot.h b/Libraries/LibWeb/DOM/ShadowRoot.h index b55b483fd2d..6093bae388b 100644 --- a/Libraries/LibWeb/DOM/ShadowRoot.h +++ b/Libraries/LibWeb/DOM/ShadowRoot.h @@ -97,6 +97,11 @@ private: GC::Ptr m_style_sheets; mutable GC::Ptr m_adopted_style_sheets; + + IntrusiveListNode m_list_node; + +public: + using DocumentShadowRootList = IntrusiveList<&ShadowRoot::m_list_node>; }; template<> From 4a5863bcdbc8bca160baed1661af54dc009b61b4 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 9 Apr 2025 22:31:12 +0200 Subject: [PATCH 31/83] LibJS: Remove unnecessary FunctionObject::name() virtual This allows us to remove the BoundFunction::m_name field, which we were initializing with a formatted FlyString on every function binding, despite never using it for anything. --- Libraries/LibJS/AST.cpp | 2 +- Libraries/LibJS/Bytecode/Interpreter.cpp | 4 ++-- Libraries/LibJS/Runtime/BoundFunction.cpp | 2 -- Libraries/LibJS/Runtime/BoundFunction.h | 3 --- Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h | 2 +- Libraries/LibJS/Runtime/FunctionObject.h | 2 -- Libraries/LibJS/Runtime/FunctionPrototype.h | 4 ---- Libraries/LibJS/Runtime/NativeFunction.h | 2 +- Libraries/LibJS/Runtime/ProxyObject.cpp | 6 ------ Libraries/LibJS/Runtime/ProxyObject.h | 1 - Libraries/LibJS/Runtime/WrappedFunction.h | 3 --- 11 files changed, 5 insertions(+), 26 deletions(-) diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 7e11a1d6bd6..1f0a40b8273 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -69,7 +69,7 @@ static void update_function_name(Value value, FlyString const& name) if (!value.is_function()) return; auto& function = value.as_function(); - if (is(function) && function.name().is_empty()) + if (is(function) && static_cast(function).name().is_empty()) static_cast(function).set_name(name); } diff --git a/Libraries/LibJS/Bytecode/Interpreter.cpp b/Libraries/LibJS/Bytecode/Interpreter.cpp index dea70a0957c..9e22a199a7b 100644 --- a/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -1212,14 +1212,14 @@ inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value thi switch (kind) { case Op::PropertyKind::Getter: { auto& function = value.as_function(); - if (function.name().is_empty() && is(function)) + if (is(function) && static_cast(function).name().is_empty()) static_cast(&function)->set_name(MUST(String::formatted("get {}", name))); object->define_direct_accessor(name, &function, nullptr, Attribute::Configurable | Attribute::Enumerable); break; } case Op::PropertyKind::Setter: { auto& function = value.as_function(); - if (function.name().is_empty() && is(function)) + if (is(function) && static_cast(function).name().is_empty()) static_cast(&function)->set_name(MUST(String::formatted("set {}", name))); object->define_direct_accessor(name, nullptr, &function, Attribute::Configurable | Attribute::Enumerable); break; diff --git a/Libraries/LibJS/Runtime/BoundFunction.cpp b/Libraries/LibJS/Runtime/BoundFunction.cpp index 0f799289b34..a8d4be7df0b 100644 --- a/Libraries/LibJS/Runtime/BoundFunction.cpp +++ b/Libraries/LibJS/Runtime/BoundFunction.cpp @@ -39,8 +39,6 @@ BoundFunction::BoundFunction(Realm& realm, FunctionObject& bound_target_function , m_bound_target_function(&bound_target_function) , m_bound_this(bound_this) , m_bound_arguments(move(bound_arguments)) - // FIXME: Non-standard and redundant, remove. - , m_name(MUST(String::formatted("bound {}", bound_target_function.name()))) { } diff --git a/Libraries/LibJS/Runtime/BoundFunction.h b/Libraries/LibJS/Runtime/BoundFunction.h index 7c48ebf4eed..9f90e3e7779 100644 --- a/Libraries/LibJS/Runtime/BoundFunction.h +++ b/Libraries/LibJS/Runtime/BoundFunction.h @@ -23,7 +23,6 @@ public: virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) override; virtual ThrowCompletionOr> internal_construct(ReadonlySpan arguments_list, FunctionObject& new_target) override; - virtual FlyString const& name() const override { return m_name; } virtual bool is_strict_mode() const override { return m_bound_target_function->is_strict_mode(); } virtual bool has_constructor() const override { return m_bound_target_function->has_constructor(); } @@ -39,8 +38,6 @@ private: GC::Ptr m_bound_target_function; // [[BoundTargetFunction]] Value m_bound_this; // [[BoundThis]] Vector m_bound_arguments; // [[BoundArguments]] - - FlyString m_name; }; } diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h index 95b053996b1..47c6397d14b 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h @@ -127,7 +127,7 @@ public: Statement const& ecmascript_code() const { return *shared_data().m_ecmascript_code; } [[nodiscard]] virtual FunctionParameters const& formal_parameters() const override { return *shared_data().m_formal_parameters; } - virtual FlyString const& name() const override { return shared_data().m_name; } + FlyString const& name() const { return shared_data().m_name; } void set_name(FlyString const& name); void set_is_class_constructor() { const_cast(shared_data()).m_is_class_constructor = true; } diff --git a/Libraries/LibJS/Runtime/FunctionObject.h b/Libraries/LibJS/Runtime/FunctionObject.h index 6af855a0f30..5d94b9449fd 100644 --- a/Libraries/LibJS/Runtime/FunctionObject.h +++ b/Libraries/LibJS/Runtime/FunctionObject.h @@ -26,8 +26,6 @@ public: virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) = 0; virtual ThrowCompletionOr> internal_construct([[maybe_unused]] ReadonlySpan arguments_list, [[maybe_unused]] FunctionObject& new_target) { VERIFY_NOT_REACHED(); } - virtual FlyString const& name() const = 0; - void set_function_name(Variant const& name_arg, Optional const& prefix = {}); void set_function_length(double length); diff --git a/Libraries/LibJS/Runtime/FunctionPrototype.h b/Libraries/LibJS/Runtime/FunctionPrototype.h index e61e89fc7bd..493fcd53ca4 100644 --- a/Libraries/LibJS/Runtime/FunctionPrototype.h +++ b/Libraries/LibJS/Runtime/FunctionPrototype.h @@ -19,7 +19,6 @@ public: virtual ~FunctionPrototype() override = default; virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) override; - virtual FlyString const& name() const override { return m_name; } private: explicit FunctionPrototype(Realm&); @@ -29,9 +28,6 @@ private: JS_DECLARE_NATIVE_FUNCTION(call); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(symbol_has_instance); - - // 20.2.3: The Function prototype object has a "name" property whose value is the empty String. - FlyString m_name; }; } diff --git a/Libraries/LibJS/Runtime/NativeFunction.h b/Libraries/LibJS/Runtime/NativeFunction.h index 6fab066c68b..19875d56ad3 100644 --- a/Libraries/LibJS/Runtime/NativeFunction.h +++ b/Libraries/LibJS/Runtime/NativeFunction.h @@ -34,7 +34,7 @@ public: virtual ThrowCompletionOr call(); virtual ThrowCompletionOr> construct(FunctionObject& new_target); - virtual FlyString const& name() const override { return m_name; } + FlyString const& name() const { return m_name; } virtual bool is_strict_mode() const override; virtual bool has_constructor() const override { return false; } virtual Realm* realm() const override { return m_realm; } diff --git a/Libraries/LibJS/Runtime/ProxyObject.cpp b/Libraries/LibJS/Runtime/ProxyObject.cpp index 60762d8e254..318742043ed 100644 --- a/Libraries/LibJS/Runtime/ProxyObject.cpp +++ b/Libraries/LibJS/Runtime/ProxyObject.cpp @@ -897,10 +897,4 @@ void ProxyObject::visit_edges(Cell::Visitor& visitor) visitor.visit(m_handler); } -FlyString const& ProxyObject::name() const -{ - VERIFY(is_function()); - return static_cast(*m_target).name(); -} - } diff --git a/Libraries/LibJS/Runtime/ProxyObject.h b/Libraries/LibJS/Runtime/ProxyObject.h index e1cf4e73d0b..e52cda49db1 100644 --- a/Libraries/LibJS/Runtime/ProxyObject.h +++ b/Libraries/LibJS/Runtime/ProxyObject.h @@ -21,7 +21,6 @@ public: virtual ~ProxyObject() override = default; - virtual FlyString const& name() const override; virtual bool has_constructor() const override; Object const& target() const { return m_target; } diff --git a/Libraries/LibJS/Runtime/WrappedFunction.h b/Libraries/LibJS/Runtime/WrappedFunction.h index 93291000794..d29e67db66a 100644 --- a/Libraries/LibJS/Runtime/WrappedFunction.h +++ b/Libraries/LibJS/Runtime/WrappedFunction.h @@ -22,9 +22,6 @@ public: virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) override; - // FIXME: Remove this (and stop inventing random internal slots that shouldn't exist, jeez) - virtual FlyString const& name() const override { return m_wrapped_target_function->name(); } - virtual Realm* realm() const override { return m_realm; } FunctionObject const& wrapped_target_function() const { return m_wrapped_target_function; } From 9dbeecb73db473f5d26e0398a49779234fdbdded Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 9 Apr 2025 12:45:18 +0100 Subject: [PATCH 32/83] LibWeb: Correct some spec typos Corresponds to https://github.com/whatwg/html/commit/285a58bf30eed08a5dd10f74a7d217fc59431999 --- Libraries/LibWeb/DOM/DocumentLoading.cpp | 2 +- Libraries/LibWeb/DOM/Element.cpp | 2 +- Libraries/LibWeb/HTML/Dates.cpp | 20 +++++++++---------- Libraries/LibWeb/HTML/EventSource.cpp | 2 +- Libraries/LibWeb/HTML/HTMLLinkElement.cpp | 2 +- Libraries/LibWeb/HTML/HTMLMediaElement.cpp | 2 +- .../HTML/Parser/HTMLEncodingDetection.cpp | 2 +- .../LibWeb/HTML/Scripting/ClassicScript.cpp | 2 +- Libraries/LibWeb/HTML/Scripting/Fetching.cpp | 2 +- .../LibWeb/HTML/WindowOrWorkerGlobalScope.cpp | 5 +++-- .../LibWeb/Page/DragAndDropEventHandler.cpp | 4 ++-- 11 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Libraries/LibWeb/DOM/DocumentLoading.cpp b/Libraries/LibWeb/DOM/DocumentLoading.cpp index 058ea46d4e8..0bc8f5853b9 100644 --- a/Libraries/LibWeb/DOM/DocumentLoading.cpp +++ b/Libraries/LibWeb/DOM/DocumentLoading.cpp @@ -448,7 +448,7 @@ GC::Ptr load_document(HTML::NavigationParams const& navigation_pa // sourceSnapshotParams, and initiatorOrigin. } - // -> A supported image, video, or audio type + // -> a supported image, video, or audio type if (type.is_image() || type.is_audio_or_video()) { // Return the result of loading a media document given navigationParams and type. diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index c4f9bc70b72..0f238ed60d4 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -2548,7 +2548,7 @@ JS::ThrowCompletionOr Element::upgrade_element(GC::Ref parse_a_month_component(GenericLexer& input) { // 1. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is // not at least four characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. - // Let that number be the year. + // Let year be that number. auto year_string = input.consume_while(is_ascii_digit); if (year_string.length() < 4) return {}; @@ -252,8 +252,8 @@ static Optional parse_a_month_component(GenericLexer& input) return {}; // 4. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is not - // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let that - // number be the month. + // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let month + // be that number. auto month_string = input.consume_while(is_ascii_digit); if (month_string.length() != 2) return {}; @@ -301,7 +301,7 @@ Optional parse_a_week_string(StringView input_view) // 3. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is // not at least four characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. - // Let that number be the year. + // Let year be that number. auto year_string = input.consume_while(is_ascii_digit); if (year_string.length() < 4) return {}; @@ -325,8 +325,8 @@ Optional parse_a_week_string(StringView input_view) return {}; // 7. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is not - // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let that - // number be the week. + // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let week + // be that number. auto week_string = input.consume_while(is_ascii_digit); if (week_string.length() != 2) return {}; @@ -365,8 +365,8 @@ static Optional parse_a_date_component(GenericLexer& input) return {}; // 4. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is not - // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let that - // number be the day. + // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let day + // be that number. auto day_string = input.consume_while(is_ascii_digit); if (day_string.length() != 2) return {}; @@ -406,7 +406,7 @@ static Optional parse_a_time_component(GenericLexer& input) { // 1. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence // is not exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten - // integer. Let that number be the hour. + // integer. Let hour be that number. auto hour_string = input.consume_while(is_ascii_digit); if (hour_string.length() != 2) return {}; @@ -426,7 +426,7 @@ static Optional parse_a_time_component(GenericLexer& input) // 4. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence // is not exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. - // Let that number be the minute. + // Let minute be that number. auto minute_string = input.consume_while(is_ascii_digit); if (minute_string.length() != 2) return {}; diff --git a/Libraries/LibWeb/HTML/EventSource.cpp b/Libraries/LibWeb/HTML/EventSource.cpp index 6bca0823226..324eec8ec51 100644 --- a/Libraries/LibWeb/HTML/EventSource.cpp +++ b/Libraries/LibWeb/HTML/EventSource.cpp @@ -389,7 +389,7 @@ void EventSource::process_field(StringView field, StringView value) { // -> If the field name is "event" if (field == "event"sv) { - // Set the event type buffer to field value. + // Set the event type buffer to the field value. m_event_type = MUST(String::from_utf8(value)); } // -> If the field name is "data" diff --git a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp index 03d2e373c67..0adacae602e 100644 --- a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp @@ -280,7 +280,7 @@ HTMLLinkElement::LinkProcessingOptions HTMLLinkElement::create_link_options() options.policy_container = document.policy_container(); // document document options.document = &document; - // FIXME: cryptographic nonce metadata The current value of el's [[CryptographicNonce]] internal slot + // FIXME: cryptographic nonce metadata the current value of el's [[CryptographicNonce]] internal slot // fetch priority the state of el's fetchpriority content attribute options.fetch_priority = Fetch::Infrastructure::request_priority_from_string(get_attribute_value(HTML::AttributeNames::fetchpriority)).value_or(Fetch::Infrastructure::Request::Priority::Auto); diff --git a/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Libraries/LibWeb/HTML/HTMLMediaElement.cpp index 5043ccbad74..b7366baf4af 100644 --- a/Libraries/LibWeb/HTML/HTMLMediaElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLMediaElement.cpp @@ -1008,7 +1008,7 @@ WebIDL::ExceptionOr HTMLMediaElement::fetch_resource(URL::URL const& url_r // 6. Let byteRange, which is "entire resource" or a (number, number or "until end") tuple, be the byte range required to satisfy missing data in // media data. This value is implementation-defined and may rely on codec, network conditions or other heuristics. The user-agent may determine // to fetch the resource in full, in which case byteRange would be "entire resource", to fetch from a byte offset until the end, in which case - // byteRange would be (number, "until end"), or to fetch a range between two byte offsets, im which case byteRange would be a (number, number) + // byteRange would be (number, "until end"), or to fetch a range between two byte offsets, in which case byteRange would be a (number, number) // tuple representing the two offsets. ByteRange byte_range = EntireResource {}; diff --git a/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp b/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp index 4d860159b78..a67b9ea0b01 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp @@ -384,7 +384,7 @@ ByteString run_encoding_sniffing_algorithm(DOM::Document& document, ByteBuffer c return maybe_transport_encoding; } - // 5. Optionally prescan the byte stream to determine its encoding, with the end condition being when the user agent decides that scanning further bytes would not + // 5. Optionally, prescan the byte stream to determine its encoding, with the end condition being when the user agent decides that scanning further bytes would not // be efficient. User agents are encouraged to only prescan the first 1024 bytes. User agents may decide that scanning any bytes is not efficient, in which case // these substeps are entirely skipped. // The aforementioned algorithm returns either a character encoding or failure. If it returns a character encoding, then return the same encoding, with confidence tentative. diff --git a/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp b/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp index e800404996b..1801eea755a 100644 --- a/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp +++ b/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp @@ -80,7 +80,7 @@ JS::Completion ClassicScript::run(RethrowErrors rethrow_errors, GC::Ptrrealm(); - // 2. Check if we can run script with realm. If this returns "do not run" then return NormalCompletion(empty). + // 2. Check if we can run script with realm. If this returns "do not run", then return NormalCompletion(empty). if (can_run_script(realm) == RunScriptDecision::DoNotRun) return JS::normal_completion(JS::js_undefined()); diff --git a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp index 24c29999f1d..e8f93ecac49 100644 --- a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp +++ b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp @@ -152,7 +152,7 @@ WebIDL::ExceptionOr resolve_module_specifier(Optional referri } } - // 11. If result is null, set result be the result of resolving an imports match given normalizedSpecifier, asURL, and importMap's imports. + // 11. If result is null, set result to the result of resolving an imports match given normalizedSpecifier, asURL, and importMap's imports. if (!result.has_value()) result = TRY(resolve_imports_match(normalized_specifier.to_byte_string(), as_url, import_map.imports())); diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index 5d95a07b8ee..2b226197edb 100644 --- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -378,7 +378,8 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler })); }; - // 13. Set uniqueHandle to the result of running steps after a timeout given global, "setTimeout/setInterval", timeout, completionStep. + // 13. Set uniqueHandle to the result of running steps after a timeout given global, "setTimeout/setInterval", + // timeout, and completionStep. // FIXME: run_steps_after_a_timeout() needs to be updated to return a unique internal value that can be used here. run_steps_after_a_timeout_impl(timeout, move(completion_step), id); @@ -994,7 +995,7 @@ void WindowOrWorkerGlobalScopeMixin::report_an_exception(JS::Value exception, Om if (false) { // FIXME: 1. Let workerObject be the Worker object associated with global. - // FIXME: 2. Set notHandled be the result of firing an event named error at workerObject, using ErrorEvent, + // FIXME: 2. Set notHandled to the result of firing an event named error at workerObject, using ErrorEvent, // with the cancelable attribute initialized to true, and additional attributes initialized // according to errorInfo. diff --git a/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp b/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp index 7a966821fbe..36d7003af9f 100644 --- a/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp +++ b/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp @@ -568,8 +568,8 @@ GC::Ref DragAndDropEventHandler::fire_a_drag_and_drop_event( if (!name.is_one_of(HTML::EventNames::dragleave, HTML::EventNames::dragend)) event_init.cancelable = true; - // 11. Initialize event's mouse and key attributes initialized according to the state of the input devices as they - // would be for user interaction events. + // 11. Initialize event's mouse and key attributes according to the state of the input devices as they would be for + // user interaction events. event_init.ctrl_key = (modifiers & UIEvents::Mod_Ctrl) != 0; event_init.shift_key = (modifiers & UIEvents::Mod_Shift) != 0; event_init.alt_key = (modifiers & UIEvents::Mod_Alt) != 0; From 23cdd78f1a84addd7df89ed53fc716753d66484a Mon Sep 17 00:00:00 2001 From: Jess Date: Thu, 20 Feb 2025 04:40:10 +1300 Subject: [PATCH 33/83] LibTest: Ensure `other` time statistic doesn't underflow --- Libraries/LibTest/TestSuite.cpp | 44 ++++++++++++++++++++------------- Libraries/LibTest/TestSuite.h | 5 ++-- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/Libraries/LibTest/TestSuite.cpp b/Libraries/LibTest/TestSuite.cpp index 4b8e0977059..0a2971cf87a 100644 --- a/Libraries/LibTest/TestSuite.cpp +++ b/Libraries/LibTest/TestSuite.cpp @@ -25,9 +25,9 @@ public: void restart() { m_started = UnixDateTime::now(); } - u64 elapsed_milliseconds() + AK::Duration elapsed() const { - return (UnixDateTime::now() - m_started).to_milliseconds(); + return UnixDateTime::now() - m_started; } private: @@ -187,17 +187,18 @@ int TestSuite::run(Vector> const& tests) m_current_test_result = TestResult::NotRun; enable_reporting(); - u64 total_time = 0; + AK::Duration total_time; u64 sum_of_squared_times = 0; - u64 min_time = NumericLimits::max(); - u64 max_time = 0; + AK::Duration min_time = AK::Duration::max(); + AK::Duration max_time; for (u64 i = 0; i < repetitions; ++i) { TestElapsedTimer timer; t->func()(); - auto const iteration_time = timer.elapsed_milliseconds(); + auto const iteration_time = timer.elapsed(); + auto const iteration_ms = iteration_time.to_milliseconds(); total_time += iteration_time; - sum_of_squared_times += iteration_time * iteration_time; + sum_of_squared_times += iteration_ms * iteration_ms; min_time = min(min_time, iteration_time); max_time = max(max_time, iteration_time); @@ -206,20 +207,26 @@ int TestSuite::run(Vector> const& tests) m_current_test_result = TestResult::Passed; } + auto const total_time_ms = total_time.to_milliseconds(); + if (repetitions != 1) { - double average = total_time / double(repetitions); + double average = total_time_ms / static_cast(repetitions); double average_squared = average * average; - double standard_deviation = sqrt((sum_of_squared_times + repetitions * average_squared - 2 * total_time * average) / (repetitions - 1)); + double standard_deviation = sqrt((sum_of_squared_times + repetitions * average_squared - 2 * total_time_ms * average) / (repetitions - 1)); dbgln("{} {} '{}' on average in {:.1f}±{:.1f}ms (min={}ms, max={}ms, total={}ms)", test_result_to_string(m_current_test_result), test_type, t->name(), - average, standard_deviation, min_time, max_time, total_time); + average, + standard_deviation, + min_time.to_milliseconds(), + max_time.to_milliseconds(), + total_time_ms); } else { - dbgln("{} {} '{}' in {}ms", test_result_to_string(m_current_test_result), test_type, t->name(), total_time); + dbgln("{} {} '{}' in {}ms", test_result_to_string(m_current_test_result), test_type, t->name(), total_time_ms); } if (t->is_benchmark()) { - m_benchtime += total_time; + m_bench_time += total_time; benchmark_count++; switch (m_current_test_result) { @@ -233,7 +240,7 @@ int TestSuite::run(Vector> const& tests) break; } } else { - m_testtime += total_time; + m_test_time += total_time; test_count++; switch (m_current_test_result) { @@ -249,13 +256,16 @@ int TestSuite::run(Vector> const& tests) } } + auto const runtime = m_test_time + m_bench_time; + auto const elapsed = global_timer.elapsed() - runtime; + dbgln("Finished {} tests and {} benchmarks in {}ms ({}ms tests, {}ms benchmarks, {}ms other).", test_count, benchmark_count, - global_timer.elapsed_milliseconds(), - m_testtime, - m_benchtime, - global_timer.elapsed_milliseconds() - (m_testtime + m_benchtime)); + elapsed.to_truncated_milliseconds(), + m_test_time.to_truncated_milliseconds(), + m_bench_time.to_truncated_milliseconds(), + (elapsed - runtime).to_truncated_milliseconds()); if (test_count != 0) { if (test_passed_count == test_count) { diff --git a/Libraries/LibTest/TestSuite.h b/Libraries/LibTest/TestSuite.h index abd39ee47d0..c293a771826 100644 --- a/Libraries/LibTest/TestSuite.h +++ b/Libraries/LibTest/TestSuite.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -65,8 +66,8 @@ public: private: static TestSuite* s_global; Vector> m_cases; - u64 m_testtime = 0; - u64 m_benchtime = 0; + AK::Duration m_test_time; + AK::Duration m_bench_time; ByteString m_suite_name; u64 m_benchmark_repetitions = 1; u64 m_randomized_runs = 100; From 3fcdbef327dec160b8b91e1425251abc8e444098 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 10 Apr 2025 10:16:13 +0100 Subject: [PATCH 34/83] =?UTF-8?q?Revert=20"LibIPC:=20Change=20TransportSoc?= =?UTF-8?q?ket=20to=20write=20large=20messages=20in=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …small chunks. This reverts commit d6080d1fdc8dcb6a7ca1c9b9ead0bd69b9d60ede. --- Libraries/LibIPC/TransportSocket.cpp | 119 +++++++++++---------------- Libraries/LibIPC/TransportSocket.h | 38 +++------ 2 files changed, 62 insertions(+), 95 deletions(-) diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 5800e165333..056d956407c 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -13,77 +13,26 @@ namespace IPC { -void SendQueue::enqueue_message(Vector&& bytes, Vector&& fds) -{ - Threading::MutexLocker locker(m_mutex); - m_bytes.append(bytes.data(), bytes.size()); - m_fds.append(fds.data(), fds.size()); - m_condition.signal(); -} - -SendQueue::Running SendQueue::block_until_message_enqueued() -{ - Threading::MutexLocker locker(m_mutex); - while (m_bytes.is_empty() && m_fds.is_empty() && m_running) - m_condition.wait(); - return m_running ? Running::Yes : Running::No; -} - -SendQueue::BytesAndFds SendQueue::dequeue(size_t max_bytes) -{ - Threading::MutexLocker locker(m_mutex); - auto bytes_to_send = min(max_bytes, m_bytes.size()); - Vector bytes; - bytes.append(m_bytes.data(), bytes_to_send); - m_bytes.remove(0, bytes_to_send); - return { move(bytes), move(m_fds) }; -} - -void SendQueue::return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds) -{ - Threading::MutexLocker locker(m_mutex); - m_bytes.prepend(bytes.data(), bytes.size()); - m_fds.prepend(fds.data(), fds.size()); -} - -void SendQueue::stop() -{ - Threading::MutexLocker locker(m_mutex); - m_running = false; - m_condition.signal(); -} - TransportSocket::TransportSocket(NonnullOwnPtr socket) : m_socket(move(socket)) { m_send_queue = adopt_ref(*new SendQueue); m_send_thread = Threading::Thread::construct([this, send_queue = m_send_queue]() -> intptr_t { for (;;) { - if (send_queue->block_until_message_enqueued() == SendQueue::Running::No) + send_queue->mutex.lock(); + while (send_queue->messages.is_empty() && send_queue->running) + send_queue->condition.wait(); + + if (!send_queue->running) { + send_queue->mutex.unlock(); break; + } - auto [bytes, fds] = send_queue->dequeue(4096); - ReadonlyBytes bytes_to_send = bytes; + auto [bytes, fds] = send_queue->messages.take_first(); + send_queue->mutex.unlock(); - auto result = send_message(*m_socket, bytes_to_send, fds); - if (result.is_error()) { + if (auto result = send_message(*m_socket, bytes, fds); result.is_error()) { dbgln("TransportSocket::send_thread: {}", result.error()); - VERIFY_NOT_REACHED(); - } - - if (!bytes.is_empty() || !fds.is_empty()) { - send_queue->return_unsent_data_to_front_of_queue(bytes_to_send, fds); - } - - { - Vector pollfds; - if (pollfds.is_empty()) - pollfds.append({ .fd = m_socket->fd().value(), .events = POLLOUT, .revents = 0 }); - - ErrorOr result { 0 }; - do { - result = Core::System::poll(pollfds, -1); - } while (result.is_error() && result.error().code() == EINTR); } } return 0; @@ -96,7 +45,11 @@ TransportSocket::TransportSocket(NonnullOwnPtr socket) TransportSocket::~TransportSocket() { - m_send_queue->stop(); + { + Threading::MutexLocker locker(m_send_queue->mutex); + m_send_queue->running = false; + m_send_queue->condition.signal(); + } (void)m_send_thread->join(); } @@ -161,27 +114,55 @@ void TransportSocket::post_message(Vector const& bytes_to_write, Vectorenqueue_message(move(message_buffer), move(raw_fds)); + queue_message_on_send_thread({ move(message_buffer), move(raw_fds) }); } -ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes& bytes_to_write, Vector& unowned_fds) +void TransportSocket::queue_message_on_send_thread(MessageToSend&& message_to_send) const +{ + Threading::MutexLocker lock(m_send_queue->mutex); + m_send_queue->messages.append(move(message_to_send)); + m_send_queue->condition.signal(); +} + +ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes&& bytes_to_write, Vector const& unowned_fds) { auto num_fds_to_transfer = unowned_fds.size(); while (!bytes_to_write.is_empty()) { ErrorOr maybe_nwritten = 0; if (num_fds_to_transfer > 0) { maybe_nwritten = socket.send_message(bytes_to_write, 0, unowned_fds); - if (!maybe_nwritten.is_error()) { + if (!maybe_nwritten.is_error()) num_fds_to_transfer = 0; - unowned_fds.clear(); - } } else { maybe_nwritten = socket.write_some(bytes_to_write); } if (maybe_nwritten.is_error()) { if (auto error = maybe_nwritten.release_error(); error.is_errno() && (error.code() == EAGAIN || error.code() == EWOULDBLOCK)) { - return {}; + + // FIXME: Refactor this to pass the unwritten bytes back to the caller to send 'later' + // or next time the socket is writable + Vector pollfds; + if (pollfds.is_empty()) + pollfds.append({ .fd = socket.fd().value(), .events = POLLOUT, .revents = 0 }); + + ErrorOr result { 0 }; + do { + constexpr u32 POLL_TIMEOUT_MS = 100; + result = Core::System::poll(pollfds, POLL_TIMEOUT_MS); + } while (result.is_error() && result.error().code() == EINTR); + + if (!result.is_error() && result.value() != 0) + continue; + + switch (error.code()) { + case EPIPE: + return Error::from_string_literal("IPC::transfer_message: Disconnected from peer"); + case EAGAIN: + return Error::from_string_literal("IPC::transfer_message: Timed out waiting for socket to become writable"); + default: + return Error::from_syscall("IPC::transfer_message write"sv, -error.code()); + } } else { return error; } @@ -271,7 +252,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib header.fd_count = received_fd_count; header.type = MessageHeader::Type::FileDescriptorAcknowledgement; memcpy(message_buffer.data(), &header, sizeof(MessageHeader)); - m_send_queue->enqueue_message(move(message_buffer), {}); + queue_message_on_send_thread({ move(message_buffer), {} }); } if (index < m_unprocessed_bytes.size()) { diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index 1c3c2a64b0d..2a4dd3a983b 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -41,31 +41,6 @@ private: int m_fd; }; -class SendQueue : public AtomicRefCounted { -public: - enum class Running { - No, - Yes, - }; - Running block_until_message_enqueued(); - void stop(); - - void enqueue_message(Vector&& bytes, Vector&& fds); - struct BytesAndFds { - Vector bytes; - Vector fds; - }; - BytesAndFds dequeue(size_t max_bytes); - void return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds); - -private: - Vector m_bytes; - Vector m_fds; - Threading::Mutex m_mutex; - Threading::ConditionVariable m_condition { m_mutex }; - bool m_running { true }; -}; - class TransportSocket { AK_MAKE_NONCOPYABLE(TransportSocket); AK_MAKE_NONMOVABLE(TransportSocket); @@ -100,7 +75,7 @@ public: ErrorOr clone_for_transfer(); private: - static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes& bytes, Vector& unowned_fds); + static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes&&, Vector const& unowned_fds); NonnullOwnPtr m_socket; ByteBuffer m_unprocessed_bytes; @@ -111,8 +86,19 @@ private: // descriptor contained in the message before the peer receives it. https://openradar.me/9477351 Queue> m_fds_retained_until_received_by_peer; + struct MessageToSend { + Vector bytes; + Vector fds; + }; + struct SendQueue : public AtomicRefCounted { + AK::SinglyLinkedList messages; + Threading::Mutex mutex; + Threading::ConditionVariable condition { mutex }; + bool running { true }; + }; RefPtr m_send_thread; RefPtr m_send_queue; + void queue_message_on_send_thread(MessageToSend&&) const; }; } From 1ee56d34e7ee1cad075fd0ebc0fe0bc4668120c3 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 10 Apr 2025 12:49:50 +0100 Subject: [PATCH 35/83] =?UTF-8?q?Revert=20"LibIPC+LibWeb:=20Delete=20Large?= =?UTF-8?q?MessageWrapper=20workaround=20in=20IPC=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …connection" This reverts commit 2d625f5c2343169d7e99f403225de47915859836. --- Libraries/LibIPC/Connection.cpp | 31 +++++++++--- Libraries/LibIPC/Connection.h | 10 ++-- Libraries/LibIPC/Decoder.h | 7 +-- Libraries/LibIPC/Message.cpp | 50 +++++++++++++++++++ Libraries/LibIPC/Message.h | 32 ++++++++++++ Libraries/LibIPC/TransportSocket.cpp | 4 +- Libraries/LibIPC/TransportSocket.h | 7 +-- Libraries/LibIPC/UnprocessedFileDescriptors.h | 36 +++++++++++++ Libraries/LibWeb/HTML/MessagePort.cpp | 10 ++-- Libraries/LibWeb/HTML/MessagePort.h | 1 + .../Tools/CodeGenerators/IPCCompiler/main.cpp | 12 +++-- 11 files changed, 177 insertions(+), 23 deletions(-) create mode 100644 Libraries/LibIPC/UnprocessedFileDescriptors.h diff --git a/Libraries/LibIPC/Connection.cpp b/Libraries/LibIPC/Connection.cpp index 8f15685393d..cc02f30d85b 100644 --- a/Libraries/LibIPC/Connection.cpp +++ b/Libraries/LibIPC/Connection.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace IPC { @@ -39,16 +40,21 @@ bool ConnectionBase::is_open() const ErrorOr ConnectionBase::post_message(Message const& message) { - return post_message(TRY(message.encode())); + return post_message(message.endpoint_magic(), TRY(message.encode())); } -ErrorOr ConnectionBase::post_message(MessageBuffer buffer) +ErrorOr ConnectionBase::post_message(u32 endpoint_magic, MessageBuffer buffer) { // NOTE: If this connection is being shut down, but has not yet been destroyed, // the socket will be closed. Don't try to send more messages. if (!m_transport->is_open()) return Error::from_string_literal("Trying to post_message during IPC shutdown"); + if (buffer.data().size() > TransportSocket::SOCKET_BUFFER_SIZE) { + auto wrapper = LargeMessageWrapper::create(endpoint_magic, buffer); + buffer = MUST(wrapper->encode()); + } + MUST(buffer.transfer_message(*m_transport)); m_responsiveness_timer->start(); @@ -79,7 +85,7 @@ void ConnectionBase::handle_messages() } if (auto response = handler_result.release_value()) { - if (auto post_result = post_message(*response); post_result.is_error()) { + if (auto post_result = post_message(m_local_endpoint_magic, *response); post_result.is_error()) { dbgln("IPC::ConnectionBase::handle_messages: {}", post_result.error()); } } @@ -94,11 +100,24 @@ void ConnectionBase::wait_for_transport_to_become_readable() ErrorOr ConnectionBase::drain_messages_from_peer() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& raw_message) { - if (auto message = try_parse_message(raw_message.bytes, raw_message.fds)) { + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& unparsed_message) { + auto const& bytes = unparsed_message.bytes; + UnprocessedFileDescriptors unprocessed_fds; + unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); + if (auto message = try_parse_message(bytes, unprocessed_fds)) { + if (message->message_id() == LargeMessageWrapper::MESSAGE_ID) { + LargeMessageWrapper* wrapper = static_cast(message.ptr()); + auto wrapped_message = wrapper->wrapped_message_data(); + unprocessed_fds.return_fds_to_front_of_queue(wrapper->take_fds()); + auto parsed_message = try_parse_message(wrapped_message, unprocessed_fds); + VERIFY(parsed_message); + m_unprocessed_messages.append(parsed_message.release_nonnull()); + return; + } + m_unprocessed_messages.append(message.release_nonnull()); } else { - dbgln("Failed to parse IPC message {:hex-dump}", raw_message.bytes); + dbgln("Failed to parse IPC message {:hex-dump}", bytes); VERIFY_NOT_REACHED(); } }); diff --git a/Libraries/LibIPC/Connection.h b/Libraries/LibIPC/Connection.h index c1644e66b6a..0f5b918226f 100644 --- a/Libraries/LibIPC/Connection.h +++ b/Libraries/LibIPC/Connection.h @@ -15,6 +15,10 @@ #include #include #include +#include +#include +#include +#include namespace IPC { @@ -26,7 +30,7 @@ public: [[nodiscard]] bool is_open() const; ErrorOr post_message(Message const&); - ErrorOr post_message(MessageBuffer); + ErrorOr post_message(u32 endpoint_magic, MessageBuffer); void shutdown(); virtual void die() { } @@ -39,7 +43,7 @@ protected: virtual void may_have_become_unresponsive() { } virtual void did_become_responsive() { } virtual void shutdown_with_error(Error const&); - virtual OwnPtr try_parse_message(ReadonlyBytes, Queue&) = 0; + virtual OwnPtr try_parse_message(ReadonlyBytes, UnprocessedFileDescriptors&) = 0; OwnPtr wait_for_specific_endpoint_message_impl(u32 endpoint_magic, int message_id); void wait_for_transport_to_become_readable(); @@ -98,7 +102,7 @@ protected: return {}; } - virtual OwnPtr try_parse_message(ReadonlyBytes bytes, Queue& fds) override + virtual OwnPtr try_parse_message(ReadonlyBytes bytes, UnprocessedFileDescriptors& fds) override { auto local_message = LocalEndpoint::decode_message(bytes, fds); if (!local_message.is_error()) diff --git a/Libraries/LibIPC/Decoder.h b/Libraries/LibIPC/Decoder.h index 7fa284c26a2..c7fdd892b72 100644 --- a/Libraries/LibIPC/Decoder.h +++ b/Libraries/LibIPC/Decoder.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -37,7 +38,7 @@ inline ErrorOr decode(Decoder&) class Decoder { public: - Decoder(Stream& stream, Queue& files) + Decoder(Stream& stream, UnprocessedFileDescriptors& files) : m_stream(stream) , m_files(files) { @@ -62,11 +63,11 @@ public: ErrorOr decode_size(); Stream& stream() { return m_stream; } - Queue& files() { return m_files; } + UnprocessedFileDescriptors& files() { return m_files; } private: Stream& m_stream; - Queue& m_files; + UnprocessedFileDescriptors& m_files; }; template diff --git a/Libraries/LibIPC/Message.cpp b/Libraries/LibIPC/Message.cpp index 680cc329d08..5652bac21cb 100644 --- a/Libraries/LibIPC/Message.cpp +++ b/Libraries/LibIPC/Message.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace IPC { @@ -46,4 +47,53 @@ ErrorOr MessageBuffer::transfer_message(Transport& transport) return {}; } +NonnullOwnPtr LargeMessageWrapper::create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap) +{ + auto size = buffer_to_wrap.data().size(); + auto wrapped_message_data = MUST(Core::AnonymousBuffer::create_with_size(size)); + memcpy(wrapped_message_data.data(), buffer_to_wrap.data().data(), size); + Vector files; + for (auto& owned_fd : buffer_to_wrap.take_fds()) { + files.append(File::adopt_fd(owned_fd->take_fd())); + } + return make(endpoint_magic, move(wrapped_message_data), move(files)); +} + +LargeMessageWrapper::LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds) + : m_endpoint_magic(endpoint_magic) + , m_wrapped_message_data(move(wrapped_message_data)) + , m_wrapped_fds(move(wrapped_fds)) +{ +} + +ErrorOr LargeMessageWrapper::encode() const +{ + MessageBuffer buffer; + Encoder stream { buffer }; + TRY(stream.encode(m_endpoint_magic)); + TRY(stream.encode(MESSAGE_ID)); + TRY(stream.encode(m_wrapped_message_data)); + TRY(stream.encode(m_wrapped_fds.size())); + for (auto const& wrapped_fd : m_wrapped_fds) { + TRY(stream.append_file_descriptor(wrapped_fd.take_fd())); + } + + return buffer; +} + +ErrorOr> LargeMessageWrapper::decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files) +{ + Decoder decoder { stream, files }; + auto wrapped_message_data = TRY(decoder.decode()); + + Vector wrapped_fds; + auto num_fds = TRY(decoder.decode()); + for (u32 i = 0; i < num_fds; ++i) { + auto fd = TRY(decoder.decode()); + wrapped_fds.append(move(fd)); + } + + return make(endpoint_magic, wrapped_message_data, move(wrapped_fds)); +} + } diff --git a/Libraries/LibIPC/Message.h b/Libraries/LibIPC/Message.h index e79638618ed..65333340f2a 100644 --- a/Libraries/LibIPC/Message.h +++ b/Libraries/LibIPC/Message.h @@ -8,8 +8,14 @@ #pragma once #include +#include +#include #include +#include +#include +#include #include +#include namespace IPC { @@ -61,4 +67,30 @@ protected: Message() = default; }; +class LargeMessageWrapper : public Message { +public: + ~LargeMessageWrapper() override = default; + + static constexpr int MESSAGE_ID = 0x0; + + static NonnullOwnPtr create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap); + + u32 endpoint_magic() const override { return m_endpoint_magic; } + int message_id() const override { return MESSAGE_ID; } + char const* message_name() const override { return "LargeMessageWrapper"; } + ErrorOr encode() const override; + + static ErrorOr> decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files); + + ReadonlyBytes wrapped_message_data() const { return ReadonlyBytes { m_wrapped_message_data.data(), m_wrapped_message_data.size() }; } + auto take_fds() { return move(m_wrapped_fds); } + + LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds); + +private: + u32 m_endpoint_magic { 0 }; + Core::AnonymousBuffer m_wrapped_message_data; + Vector m_wrapped_fds; +}; + } diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 056d956407c..86239539ce7 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -173,7 +173,7 @@ ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyB return {}; } -TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) +TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) { bool should_shutdown = false; while (is_open()) { @@ -222,7 +222,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib Message message; received_fd_count += header.fd_count; for (size_t i = 0; i < header.fd_count; ++i) - message.fds.enqueue(m_unprocessed_fds.dequeue()); + message.fds.append(m_unprocessed_fds.dequeue()); message.bytes.append(m_unprocessed_bytes.data() + index + sizeof(MessageHeader), header.payload_size); callback(move(message)); } else if (header.type == MessageHeader::Type::FileDescriptorAcknowledgement) { diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index 2a4dd3a983b..2c466d0f08c 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -65,9 +66,9 @@ public: }; struct Message { Vector bytes; - Queue fds; + Vector fds; }; - ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&& schedule_shutdown); + ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&& schedule_shutdown); // Obnoxious name to make it clear that this is a dangerous operation. ErrorOr release_underlying_transport_for_transfer(); @@ -79,7 +80,7 @@ private: NonnullOwnPtr m_socket; ByteBuffer m_unprocessed_bytes; - Queue m_unprocessed_fds; + UnprocessedFileDescriptors m_unprocessed_fds; // After file descriptor is sent, it is moved to the wait queue until an acknowledgement is received from the peer. // This is necessary to handle a specific behavior of the macOS kernel, which may prematurely garbage-collect the file diff --git a/Libraries/LibIPC/UnprocessedFileDescriptors.h b/Libraries/LibIPC/UnprocessedFileDescriptors.h new file mode 100644 index 00000000000..991c5598b8a --- /dev/null +++ b/Libraries/LibIPC/UnprocessedFileDescriptors.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace IPC { + +class UnprocessedFileDescriptors { +public: + void enqueue(File&& fd) + { + m_fds.append(move(fd)); + } + + File dequeue() + { + return m_fds.take_first(); + } + + void return_fds_to_front_of_queue(Vector&& fds) + { + m_fds.prepend(move(fds)); + } + + size_t size() const { return m_fds.size(); } + +private: + Vector m_fds; +}; + +} diff --git a/Libraries/LibWeb/HTML/MessagePort.cpp b/Libraries/LibWeb/HTML/MessagePort.cpp index 97d6a808e80..03fe434b6ec 100644 --- a/Libraries/LibWeb/HTML/MessagePort.cpp +++ b/Libraries/LibWeb/HTML/MessagePort.cpp @@ -288,9 +288,13 @@ void MessagePort::post_port_message(SerializedTransferRecord serialize_with_tran void MessagePort::read_from_transport() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& raw_message) { - FixedMemoryStream stream { raw_message.bytes.span(), FixedMemoryStream::Mode::ReadOnly }; - IPC::Decoder decoder { stream, raw_message.fds }; + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& unparsed_message) { + auto& bytes = unparsed_message.bytes; + IPC::UnprocessedFileDescriptors unprocessed_fds; + unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); + + FixedMemoryStream stream { bytes.span(), FixedMemoryStream::Mode::ReadOnly }; + IPC::Decoder decoder { stream, unprocessed_fds }; auto serialized_transfer_record = MUST(decoder.decode()); diff --git a/Libraries/LibWeb/HTML/MessagePort.h b/Libraries/LibWeb/HTML/MessagePort.h index 24e3bbeccbf..0d48fd0adfb 100644 --- a/Libraries/LibWeb/HTML/MessagePort.h +++ b/Libraries/LibWeb/HTML/MessagePort.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include diff --git a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp index c7fe62dfa4a..4d041620171 100644 --- a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp @@ -404,7 +404,7 @@ public:)~~~"); static i32 static_message_id() { return (int)MessageID::@message.pascal_name@; } virtual const char* message_name() const override { return "@endpoint.name@::@message.pascal_name@"; } - static ErrorOr> decode(Stream& stream, Queue& files) + static ErrorOr> decode(Stream& stream, IPC::UnprocessedFileDescriptors& files) { IPC::Decoder decoder { stream, files };)~~~"); @@ -649,7 +649,7 @@ void generate_proxy_method(SourceGenerator& message_generator, Endpoint const& e } } else { message_generator.append(R"~~~()); - MUST(m_connection.post_message(move(message_buffer))); )~~~"); + MUST(m_connection.post_message(@endpoint.magic@, move(message_buffer))); )~~~"); } message_generator.appendln(R"~~~( @@ -720,7 +720,7 @@ public: static u32 static_magic() { return @endpoint.magic@; } - static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] Queue& files) + static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] IPC::UnprocessedFileDescriptors& files) { FixedMemoryStream stream { buffer }; auto message_endpoint_magic = TRY(stream.read_value());)~~~"); @@ -757,6 +757,11 @@ public: do_decode_message(message.response_name()); } + generator.append(R"~~~( + case (int)IPC::LargeMessageWrapper::MESSAGE_ID: + return TRY(IPC::LargeMessageWrapper::decode(message_endpoint_magic, stream, files)); +)~~~"); + generator.append(R"~~~( default:)~~~"); if constexpr (GENERATE_DEBUG) { @@ -898,6 +903,7 @@ void build(StringBuilder& builder, Vector const& endpoints) #include #include #include +#include #if defined(AK_COMPILER_CLANG) #pragma clang diagnostic push From 14dc7686c3c1e6936c38e07239211c57ca0a5b33 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Wed, 9 Apr 2025 20:54:41 +0200 Subject: [PATCH 36/83] LibIPC: Change TransportSocket to write large messages in small chunks Bring back d6080d1fdc8dcb6a7ca1c9b9ead0bd69b9d60ede with a missing check whether underlying socket is closed, before accessing `fd()` that is optional and empty in case of closed socket. --- Libraries/LibIPC/TransportSocket.cpp | 123 ++++++++++++++++----------- Libraries/LibIPC/TransportSocket.h | 40 ++++++--- 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 86239539ce7..67c32044488 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -13,26 +13,79 @@ namespace IPC { +void SendQueue::enqueue_message(Vector&& bytes, Vector&& fds) +{ + Threading::MutexLocker locker(m_mutex); + m_bytes.append(bytes.data(), bytes.size()); + m_fds.append(fds.data(), fds.size()); + m_condition.signal(); +} + +SendQueue::Running SendQueue::block_until_message_enqueued() +{ + Threading::MutexLocker locker(m_mutex); + while (m_bytes.is_empty() && m_fds.is_empty() && m_running) + m_condition.wait(); + return m_running ? Running::Yes : Running::No; +} + +SendQueue::BytesAndFds SendQueue::dequeue(size_t max_bytes) +{ + Threading::MutexLocker locker(m_mutex); + auto bytes_to_send = min(max_bytes, m_bytes.size()); + Vector bytes; + bytes.append(m_bytes.data(), bytes_to_send); + m_bytes.remove(0, bytes_to_send); + return { move(bytes), move(m_fds) }; +} + +void SendQueue::return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds) +{ + Threading::MutexLocker locker(m_mutex); + m_bytes.prepend(bytes.data(), bytes.size()); + m_fds.prepend(fds.data(), fds.size()); +} + +void SendQueue::stop() +{ + Threading::MutexLocker locker(m_mutex); + m_running = false; + m_condition.signal(); +} + TransportSocket::TransportSocket(NonnullOwnPtr socket) : m_socket(move(socket)) { m_send_queue = adopt_ref(*new SendQueue); m_send_thread = Threading::Thread::construct([this, send_queue = m_send_queue]() -> intptr_t { for (;;) { - send_queue->mutex.lock(); - while (send_queue->messages.is_empty() && send_queue->running) - send_queue->condition.wait(); - - if (!send_queue->running) { - send_queue->mutex.unlock(); + if (send_queue->block_until_message_enqueued() == SendQueue::Running::No) break; + + auto [bytes, fds] = send_queue->dequeue(4096); + ReadonlyBytes remaining_to_send_bytes = bytes; + + auto result = send_message(*m_socket, remaining_to_send_bytes, fds); + if (result.is_error()) { + dbgln("TransportSocket::send_thread: {}", result.error()); + VERIFY_NOT_REACHED(); } - auto [bytes, fds] = send_queue->messages.take_first(); - send_queue->mutex.unlock(); + if (!remaining_to_send_bytes.is_empty() || !fds.is_empty()) { + send_queue->return_unsent_data_to_front_of_queue(remaining_to_send_bytes, fds); + } - if (auto result = send_message(*m_socket, bytes, fds); result.is_error()) { - dbgln("TransportSocket::send_thread: {}", result.error()); + if (!m_socket->is_open()) + break; + + { + Vector pollfds; + pollfds.append({ .fd = m_socket->fd().value(), .events = POLLOUT, .revents = 0 }); + + ErrorOr result { 0 }; + do { + result = Core::System::poll(pollfds, -1); + } while (result.is_error() && result.error().code() == EINTR); } } return 0; @@ -45,11 +98,7 @@ TransportSocket::TransportSocket(NonnullOwnPtr socket) TransportSocket::~TransportSocket() { - { - Threading::MutexLocker locker(m_send_queue->mutex); - m_send_queue->running = false; - m_send_queue->condition.signal(); - } + m_send_queue->stop(); (void)m_send_thread->join(); } @@ -114,61 +163,31 @@ void TransportSocket::post_message(Vector const& bytes_to_write, Vectorenqueue_message(move(message_buffer), move(raw_fds)); } -void TransportSocket::queue_message_on_send_thread(MessageToSend&& message_to_send) const -{ - Threading::MutexLocker lock(m_send_queue->mutex); - m_send_queue->messages.append(move(message_to_send)); - m_send_queue->condition.signal(); -} - -ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes&& bytes_to_write, Vector const& unowned_fds) +ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes& bytes_to_write, Vector& unowned_fds) { auto num_fds_to_transfer = unowned_fds.size(); while (!bytes_to_write.is_empty()) { ErrorOr maybe_nwritten = 0; if (num_fds_to_transfer > 0) { maybe_nwritten = socket.send_message(bytes_to_write, 0, unowned_fds); - if (!maybe_nwritten.is_error()) - num_fds_to_transfer = 0; } else { maybe_nwritten = socket.write_some(bytes_to_write); } if (maybe_nwritten.is_error()) { - if (auto error = maybe_nwritten.release_error(); error.is_errno() && (error.code() == EAGAIN || error.code() == EWOULDBLOCK)) { - - // FIXME: Refactor this to pass the unwritten bytes back to the caller to send 'later' - // or next time the socket is writable - Vector pollfds; - if (pollfds.is_empty()) - pollfds.append({ .fd = socket.fd().value(), .events = POLLOUT, .revents = 0 }); - - ErrorOr result { 0 }; - do { - constexpr u32 POLL_TIMEOUT_MS = 100; - result = Core::System::poll(pollfds, POLL_TIMEOUT_MS); - } while (result.is_error() && result.error().code() == EINTR); - - if (!result.is_error() && result.value() != 0) - continue; - - switch (error.code()) { - case EPIPE: - return Error::from_string_literal("IPC::transfer_message: Disconnected from peer"); - case EAGAIN: - return Error::from_string_literal("IPC::transfer_message: Timed out waiting for socket to become writable"); - default: - return Error::from_syscall("IPC::transfer_message write"sv, -error.code()); - } + if (auto error = maybe_nwritten.release_error(); error.is_errno() && (error.code() == EAGAIN || error.code() == EWOULDBLOCK || error.code() == EINTR)) { + return {}; } else { return error; } } bytes_to_write = bytes_to_write.slice(maybe_nwritten.value()); + num_fds_to_transfer = 0; + unowned_fds.clear(); } return {}; } @@ -252,7 +271,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib header.fd_count = received_fd_count; header.type = MessageHeader::Type::FileDescriptorAcknowledgement; memcpy(message_buffer.data(), &header, sizeof(MessageHeader)); - queue_message_on_send_thread({ move(message_buffer), {} }); + m_send_queue->enqueue_message(move(message_buffer), {}); } if (index < m_unprocessed_bytes.size()) { diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index 2c466d0f08c..b980fdb739c 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -42,6 +42,31 @@ private: int m_fd; }; +class SendQueue : public AtomicRefCounted { +public: + enum class Running { + No, + Yes, + }; + Running block_until_message_enqueued(); + void stop(); + + void enqueue_message(Vector&& bytes, Vector&& fds); + struct BytesAndFds { + Vector bytes; + Vector fds; + }; + BytesAndFds dequeue(size_t max_bytes); + void return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds); + +private: + Vector m_bytes; + Vector m_fds; + Threading::Mutex m_mutex; + Threading::ConditionVariable m_condition { m_mutex }; + bool m_running { true }; +}; + class TransportSocket { AK_MAKE_NONCOPYABLE(TransportSocket); AK_MAKE_NONMOVABLE(TransportSocket); @@ -68,7 +93,7 @@ public: Vector bytes; Vector fds; }; - ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&& schedule_shutdown); + ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&&); // Obnoxious name to make it clear that this is a dangerous operation. ErrorOr release_underlying_transport_for_transfer(); @@ -76,7 +101,7 @@ public: ErrorOr clone_for_transfer(); private: - static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes&&, Vector const& unowned_fds); + static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes& bytes, Vector& unowned_fds); NonnullOwnPtr m_socket; ByteBuffer m_unprocessed_bytes; @@ -87,19 +112,8 @@ private: // descriptor contained in the message before the peer receives it. https://openradar.me/9477351 Queue> m_fds_retained_until_received_by_peer; - struct MessageToSend { - Vector bytes; - Vector fds; - }; - struct SendQueue : public AtomicRefCounted { - AK::SinglyLinkedList messages; - Threading::Mutex mutex; - Threading::ConditionVariable condition { mutex }; - bool running { true }; - }; RefPtr m_send_thread; RefPtr m_send_queue; - void queue_message_on_send_thread(MessageToSend&&) const; }; } From 681333d329867ecf36311f2201952a364ae5a61d Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 10 Apr 2025 22:05:11 +0200 Subject: [PATCH 37/83] LibIPC: Protect underlying socket of TransportSocket with RWLock This is necessary to prevent the socket from being closed while it is being used for reading or writing. --- Libraries/LibIPC/TransportSocket.cpp | 9 +++++++++ Libraries/LibIPC/TransportSocket.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 67c32044488..96d9ed78eb0 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -65,6 +65,7 @@ TransportSocket::TransportSocket(NonnullOwnPtr socket) auto [bytes, fds] = send_queue->dequeue(4096); ReadonlyBytes remaining_to_send_bytes = bytes; + Threading::RWLockLocker lock(m_socket_rw_lock); auto result = send_message(*m_socket, remaining_to_send_bytes, fds); if (result.is_error()) { dbgln("TransportSocket::send_thread: {}", result.error()); @@ -104,22 +105,26 @@ TransportSocket::~TransportSocket() void TransportSocket::set_up_read_hook(Function hook) { + Threading::RWLockLocker lock(m_socket_rw_lock); VERIFY(m_socket->is_open()); m_socket->on_ready_to_read = move(hook); } bool TransportSocket::is_open() const { + Threading::RWLockLocker lock(m_socket_rw_lock); return m_socket->is_open(); } void TransportSocket::close() { + Threading::RWLockLocker lock(m_socket_rw_lock); m_socket->close(); } void TransportSocket::wait_until_readable() { + Threading::RWLockLocker lock(m_socket_rw_lock); auto maybe_did_become_readable = m_socket->can_read_without_blocking(-1); if (maybe_did_become_readable.is_error()) { dbgln("TransportSocket::wait_until_readable: {}", maybe_did_become_readable.error()); @@ -194,6 +199,8 @@ ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyB TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) { + Threading::RWLockLocker lock(m_socket_rw_lock); + bool should_shutdown = false; while (is_open()) { u8 buffer[4096]; @@ -286,11 +293,13 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib ErrorOr TransportSocket::release_underlying_transport_for_transfer() { + Threading::RWLockLocker lock(m_socket_rw_lock); return m_socket->release_fd(); } ErrorOr TransportSocket::clone_for_transfer() { + Threading::RWLockLocker lock(m_socket_rw_lock); return IPC::File::clone_fd(m_socket->fd().value()); } diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index b980fdb739c..d60c4dfebd0 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace IPC { @@ -104,6 +105,7 @@ private: static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes& bytes, Vector& unowned_fds); NonnullOwnPtr m_socket; + mutable Threading::RWLock m_socket_rw_lock; ByteBuffer m_unprocessed_bytes; UnprocessedFileDescriptors m_unprocessed_fds; From b53694b4c098a44984ddbee4e654a1272968de7d Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 10 Apr 2025 20:26:46 +0200 Subject: [PATCH 38/83] LibIPC+LibWeb: Delete LargeMessageWrapper workaround in IPC connection Bring back 2d625f5c2343169d7e99f403225de47915859836 --- Libraries/LibIPC/Connection.cpp | 31 +++--------- Libraries/LibIPC/Connection.h | 10 ++-- Libraries/LibIPC/Decoder.h | 7 ++- Libraries/LibIPC/Message.cpp | 50 ------------------- Libraries/LibIPC/Message.h | 32 ------------ Libraries/LibIPC/TransportSocket.cpp | 4 +- Libraries/LibIPC/TransportSocket.h | 7 ++- Libraries/LibIPC/UnprocessedFileDescriptors.h | 36 ------------- Libraries/LibWeb/HTML/MessagePort.cpp | 10 ++-- Libraries/LibWeb/HTML/MessagePort.h | 1 - .../Tools/CodeGenerators/IPCCompiler/main.cpp | 12 ++--- 11 files changed, 23 insertions(+), 177 deletions(-) delete mode 100644 Libraries/LibIPC/UnprocessedFileDescriptors.h diff --git a/Libraries/LibIPC/Connection.cpp b/Libraries/LibIPC/Connection.cpp index cc02f30d85b..8f15685393d 100644 --- a/Libraries/LibIPC/Connection.cpp +++ b/Libraries/LibIPC/Connection.cpp @@ -12,7 +12,6 @@ #include #include #include -#include namespace IPC { @@ -40,21 +39,16 @@ bool ConnectionBase::is_open() const ErrorOr ConnectionBase::post_message(Message const& message) { - return post_message(message.endpoint_magic(), TRY(message.encode())); + return post_message(TRY(message.encode())); } -ErrorOr ConnectionBase::post_message(u32 endpoint_magic, MessageBuffer buffer) +ErrorOr ConnectionBase::post_message(MessageBuffer buffer) { // NOTE: If this connection is being shut down, but has not yet been destroyed, // the socket will be closed. Don't try to send more messages. if (!m_transport->is_open()) return Error::from_string_literal("Trying to post_message during IPC shutdown"); - if (buffer.data().size() > TransportSocket::SOCKET_BUFFER_SIZE) { - auto wrapper = LargeMessageWrapper::create(endpoint_magic, buffer); - buffer = MUST(wrapper->encode()); - } - MUST(buffer.transfer_message(*m_transport)); m_responsiveness_timer->start(); @@ -85,7 +79,7 @@ void ConnectionBase::handle_messages() } if (auto response = handler_result.release_value()) { - if (auto post_result = post_message(m_local_endpoint_magic, *response); post_result.is_error()) { + if (auto post_result = post_message(*response); post_result.is_error()) { dbgln("IPC::ConnectionBase::handle_messages: {}", post_result.error()); } } @@ -100,24 +94,11 @@ void ConnectionBase::wait_for_transport_to_become_readable() ErrorOr ConnectionBase::drain_messages_from_peer() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& unparsed_message) { - auto const& bytes = unparsed_message.bytes; - UnprocessedFileDescriptors unprocessed_fds; - unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); - if (auto message = try_parse_message(bytes, unprocessed_fds)) { - if (message->message_id() == LargeMessageWrapper::MESSAGE_ID) { - LargeMessageWrapper* wrapper = static_cast(message.ptr()); - auto wrapped_message = wrapper->wrapped_message_data(); - unprocessed_fds.return_fds_to_front_of_queue(wrapper->take_fds()); - auto parsed_message = try_parse_message(wrapped_message, unprocessed_fds); - VERIFY(parsed_message); - m_unprocessed_messages.append(parsed_message.release_nonnull()); - return; - } - + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& raw_message) { + if (auto message = try_parse_message(raw_message.bytes, raw_message.fds)) { m_unprocessed_messages.append(message.release_nonnull()); } else { - dbgln("Failed to parse IPC message {:hex-dump}", bytes); + dbgln("Failed to parse IPC message {:hex-dump}", raw_message.bytes); VERIFY_NOT_REACHED(); } }); diff --git a/Libraries/LibIPC/Connection.h b/Libraries/LibIPC/Connection.h index 0f5b918226f..c1644e66b6a 100644 --- a/Libraries/LibIPC/Connection.h +++ b/Libraries/LibIPC/Connection.h @@ -15,10 +15,6 @@ #include #include #include -#include -#include -#include -#include namespace IPC { @@ -30,7 +26,7 @@ public: [[nodiscard]] bool is_open() const; ErrorOr post_message(Message const&); - ErrorOr post_message(u32 endpoint_magic, MessageBuffer); + ErrorOr post_message(MessageBuffer); void shutdown(); virtual void die() { } @@ -43,7 +39,7 @@ protected: virtual void may_have_become_unresponsive() { } virtual void did_become_responsive() { } virtual void shutdown_with_error(Error const&); - virtual OwnPtr try_parse_message(ReadonlyBytes, UnprocessedFileDescriptors&) = 0; + virtual OwnPtr try_parse_message(ReadonlyBytes, Queue&) = 0; OwnPtr wait_for_specific_endpoint_message_impl(u32 endpoint_magic, int message_id); void wait_for_transport_to_become_readable(); @@ -102,7 +98,7 @@ protected: return {}; } - virtual OwnPtr try_parse_message(ReadonlyBytes bytes, UnprocessedFileDescriptors& fds) override + virtual OwnPtr try_parse_message(ReadonlyBytes bytes, Queue& fds) override { auto local_message = LocalEndpoint::decode_message(bytes, fds); if (!local_message.is_error()) diff --git a/Libraries/LibIPC/Decoder.h b/Libraries/LibIPC/Decoder.h index c7fdd892b72..7fa284c26a2 100644 --- a/Libraries/LibIPC/Decoder.h +++ b/Libraries/LibIPC/Decoder.h @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -38,7 +37,7 @@ inline ErrorOr decode(Decoder&) class Decoder { public: - Decoder(Stream& stream, UnprocessedFileDescriptors& files) + Decoder(Stream& stream, Queue& files) : m_stream(stream) , m_files(files) { @@ -63,11 +62,11 @@ public: ErrorOr decode_size(); Stream& stream() { return m_stream; } - UnprocessedFileDescriptors& files() { return m_files; } + Queue& files() { return m_files; } private: Stream& m_stream; - UnprocessedFileDescriptors& m_files; + Queue& m_files; }; template diff --git a/Libraries/LibIPC/Message.cpp b/Libraries/LibIPC/Message.cpp index 5652bac21cb..680cc329d08 100644 --- a/Libraries/LibIPC/Message.cpp +++ b/Libraries/LibIPC/Message.cpp @@ -6,7 +6,6 @@ #include #include -#include #include namespace IPC { @@ -47,53 +46,4 @@ ErrorOr MessageBuffer::transfer_message(Transport& transport) return {}; } -NonnullOwnPtr LargeMessageWrapper::create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap) -{ - auto size = buffer_to_wrap.data().size(); - auto wrapped_message_data = MUST(Core::AnonymousBuffer::create_with_size(size)); - memcpy(wrapped_message_data.data(), buffer_to_wrap.data().data(), size); - Vector files; - for (auto& owned_fd : buffer_to_wrap.take_fds()) { - files.append(File::adopt_fd(owned_fd->take_fd())); - } - return make(endpoint_magic, move(wrapped_message_data), move(files)); -} - -LargeMessageWrapper::LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds) - : m_endpoint_magic(endpoint_magic) - , m_wrapped_message_data(move(wrapped_message_data)) - , m_wrapped_fds(move(wrapped_fds)) -{ -} - -ErrorOr LargeMessageWrapper::encode() const -{ - MessageBuffer buffer; - Encoder stream { buffer }; - TRY(stream.encode(m_endpoint_magic)); - TRY(stream.encode(MESSAGE_ID)); - TRY(stream.encode(m_wrapped_message_data)); - TRY(stream.encode(m_wrapped_fds.size())); - for (auto const& wrapped_fd : m_wrapped_fds) { - TRY(stream.append_file_descriptor(wrapped_fd.take_fd())); - } - - return buffer; -} - -ErrorOr> LargeMessageWrapper::decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files) -{ - Decoder decoder { stream, files }; - auto wrapped_message_data = TRY(decoder.decode()); - - Vector wrapped_fds; - auto num_fds = TRY(decoder.decode()); - for (u32 i = 0; i < num_fds; ++i) { - auto fd = TRY(decoder.decode()); - wrapped_fds.append(move(fd)); - } - - return make(endpoint_magic, wrapped_message_data, move(wrapped_fds)); -} - } diff --git a/Libraries/LibIPC/Message.h b/Libraries/LibIPC/Message.h index 65333340f2a..e79638618ed 100644 --- a/Libraries/LibIPC/Message.h +++ b/Libraries/LibIPC/Message.h @@ -8,14 +8,8 @@ #pragma once #include -#include -#include #include -#include -#include -#include #include -#include namespace IPC { @@ -67,30 +61,4 @@ protected: Message() = default; }; -class LargeMessageWrapper : public Message { -public: - ~LargeMessageWrapper() override = default; - - static constexpr int MESSAGE_ID = 0x0; - - static NonnullOwnPtr create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap); - - u32 endpoint_magic() const override { return m_endpoint_magic; } - int message_id() const override { return MESSAGE_ID; } - char const* message_name() const override { return "LargeMessageWrapper"; } - ErrorOr encode() const override; - - static ErrorOr> decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files); - - ReadonlyBytes wrapped_message_data() const { return ReadonlyBytes { m_wrapped_message_data.data(), m_wrapped_message_data.size() }; } - auto take_fds() { return move(m_wrapped_fds); } - - LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds); - -private: - u32 m_endpoint_magic { 0 }; - Core::AnonymousBuffer m_wrapped_message_data; - Vector m_wrapped_fds; -}; - } diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 96d9ed78eb0..0c7a2064cd4 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -197,7 +197,7 @@ ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyB return {}; } -TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) +TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) { Threading::RWLockLocker lock(m_socket_rw_lock); @@ -248,7 +248,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib Message message; received_fd_count += header.fd_count; for (size_t i = 0; i < header.fd_count; ++i) - message.fds.append(m_unprocessed_fds.dequeue()); + message.fds.enqueue(m_unprocessed_fds.dequeue()); message.bytes.append(m_unprocessed_bytes.data() + index + sizeof(MessageHeader), header.payload_size); callback(move(message)); } else if (header.type == MessageHeader::Type::FileDescriptorAcknowledgement) { diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index d60c4dfebd0..e7a19f4e159 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -92,9 +91,9 @@ public: }; struct Message { Vector bytes; - Vector fds; + Queue fds; }; - ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&&); + ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&&); // Obnoxious name to make it clear that this is a dangerous operation. ErrorOr release_underlying_transport_for_transfer(); @@ -107,7 +106,7 @@ private: NonnullOwnPtr m_socket; mutable Threading::RWLock m_socket_rw_lock; ByteBuffer m_unprocessed_bytes; - UnprocessedFileDescriptors m_unprocessed_fds; + Queue m_unprocessed_fds; // After file descriptor is sent, it is moved to the wait queue until an acknowledgement is received from the peer. // This is necessary to handle a specific behavior of the macOS kernel, which may prematurely garbage-collect the file diff --git a/Libraries/LibIPC/UnprocessedFileDescriptors.h b/Libraries/LibIPC/UnprocessedFileDescriptors.h deleted file mode 100644 index 991c5598b8a..00000000000 --- a/Libraries/LibIPC/UnprocessedFileDescriptors.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2025, Aliaksandr Kalenik - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include - -namespace IPC { - -class UnprocessedFileDescriptors { -public: - void enqueue(File&& fd) - { - m_fds.append(move(fd)); - } - - File dequeue() - { - return m_fds.take_first(); - } - - void return_fds_to_front_of_queue(Vector&& fds) - { - m_fds.prepend(move(fds)); - } - - size_t size() const { return m_fds.size(); } - -private: - Vector m_fds; -}; - -} diff --git a/Libraries/LibWeb/HTML/MessagePort.cpp b/Libraries/LibWeb/HTML/MessagePort.cpp index 03fe434b6ec..97d6a808e80 100644 --- a/Libraries/LibWeb/HTML/MessagePort.cpp +++ b/Libraries/LibWeb/HTML/MessagePort.cpp @@ -288,13 +288,9 @@ void MessagePort::post_port_message(SerializedTransferRecord serialize_with_tran void MessagePort::read_from_transport() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& unparsed_message) { - auto& bytes = unparsed_message.bytes; - IPC::UnprocessedFileDescriptors unprocessed_fds; - unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); - - FixedMemoryStream stream { bytes.span(), FixedMemoryStream::Mode::ReadOnly }; - IPC::Decoder decoder { stream, unprocessed_fds }; + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& raw_message) { + FixedMemoryStream stream { raw_message.bytes.span(), FixedMemoryStream::Mode::ReadOnly }; + IPC::Decoder decoder { stream, raw_message.fds }; auto serialized_transfer_record = MUST(decoder.decode()); diff --git a/Libraries/LibWeb/HTML/MessagePort.h b/Libraries/LibWeb/HTML/MessagePort.h index 0d48fd0adfb..24e3bbeccbf 100644 --- a/Libraries/LibWeb/HTML/MessagePort.h +++ b/Libraries/LibWeb/HTML/MessagePort.h @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp index 4d041620171..c7fe62dfa4a 100644 --- a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp @@ -404,7 +404,7 @@ public:)~~~"); static i32 static_message_id() { return (int)MessageID::@message.pascal_name@; } virtual const char* message_name() const override { return "@endpoint.name@::@message.pascal_name@"; } - static ErrorOr> decode(Stream& stream, IPC::UnprocessedFileDescriptors& files) + static ErrorOr> decode(Stream& stream, Queue& files) { IPC::Decoder decoder { stream, files };)~~~"); @@ -649,7 +649,7 @@ void generate_proxy_method(SourceGenerator& message_generator, Endpoint const& e } } else { message_generator.append(R"~~~()); - MUST(m_connection.post_message(@endpoint.magic@, move(message_buffer))); )~~~"); + MUST(m_connection.post_message(move(message_buffer))); )~~~"); } message_generator.appendln(R"~~~( @@ -720,7 +720,7 @@ public: static u32 static_magic() { return @endpoint.magic@; } - static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] IPC::UnprocessedFileDescriptors& files) + static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] Queue& files) { FixedMemoryStream stream { buffer }; auto message_endpoint_magic = TRY(stream.read_value());)~~~"); @@ -757,11 +757,6 @@ public: do_decode_message(message.response_name()); } - generator.append(R"~~~( - case (int)IPC::LargeMessageWrapper::MESSAGE_ID: - return TRY(IPC::LargeMessageWrapper::decode(message_endpoint_magic, stream, files)); -)~~~"); - generator.append(R"~~~( default:)~~~"); if constexpr (GENERATE_DEBUG) { @@ -903,7 +898,6 @@ void build(StringBuilder& builder, Vector const& endpoints) #include #include #include -#include #if defined(AK_COMPILER_CLANG) #pragma clang diagnostic push From 33457f389d7bd5e0fa2f0ebf343c2a0399874161 Mon Sep 17 00:00:00 2001 From: stasoid Date: Wed, 8 Jan 2025 11:41:50 +0500 Subject: [PATCH 39/83] LibCore: Check for null ThreadData in unregister_notifier and unregister_timer in EventLoopManagerWindows Destructors for thread local objects are called before destructors of global not thread local objects. This is a partial stack of the problem, thread_data is already destroyed at this point: >WebContent.exe!Core::ThreadData::the WebContent.exe!Core::EventLoopManagerWindows::unregister_notifier WebContent.exe!Core::EventLoop::unregister_notifier WebContent.exe!Core::Notifier::set_enabled WebContent.exe!Core::LocalSocket::~LocalSocket WebContent.exe!Requests::RequestClient::~RequestClient WebContent.exe!Web::`dynamic atexit destructor for 's_resource_loader' --- .../EventLoopImplementationWindows.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Libraries/LibCore/EventLoopImplementationWindows.cpp b/Libraries/LibCore/EventLoopImplementationWindows.cpp index 8ca0ec207d8..37afaf01dba 100644 --- a/Libraries/LibCore/EventLoopImplementationWindows.cpp +++ b/Libraries/LibCore/EventLoopImplementationWindows.cpp @@ -55,10 +55,12 @@ struct EventLoopTimer { }; struct ThreadData { - static ThreadData& the() + static ThreadData* the() { thread_local OwnPtr thread_data = make(); - return *thread_data; + if (thread_data) + return &*thread_data; + return nullptr; } ThreadData() @@ -76,7 +78,7 @@ struct ThreadData { }; EventLoopImplementationWindows::EventLoopImplementationWindows() - : m_wake_event(ThreadData::the().wake_event.handle) + : m_wake_event(ThreadData::the()->wake_event.handle) { } @@ -92,9 +94,9 @@ int EventLoopImplementationWindows::exec() size_t EventLoopImplementationWindows::pump(PumpMode) { - auto& thread_data = ThreadData::the(); - auto& notifiers = thread_data.notifiers; - auto& timers = thread_data.timers; + auto thread_data = ThreadData::the(); + auto& notifiers = thread_data->notifiers; + auto& timers = thread_data->timers; size_t event_count = 1 + notifiers.size() + timers.size(); // If 64 events limit proves to be insufficient RegisterWaitForSingleObject or other methods @@ -103,7 +105,7 @@ size_t EventLoopImplementationWindows::pump(PumpMode) VERIFY(event_count <= MAXIMUM_WAIT_OBJECTS); Vector event_handles; - event_handles.append(thread_data.wake_event.handle); + event_handles.append(thread_data->wake_event.handle); for (auto& entry : notifiers) event_handles.append(entry.key.handle); @@ -167,7 +169,7 @@ void EventLoopManagerWindows::register_notifier(Notifier& notifier) int rc = WSAEventSelect(notifier.fd(), event, notifier_type_to_network_event(notifier.type())); VERIFY(!rc); - auto& notifiers = ThreadData::the().notifiers; + auto& notifiers = ThreadData::the()->notifiers; VERIFY(!notifiers.get(event).has_value()); notifiers.set(Handle(event), ¬ifier); } @@ -175,7 +177,8 @@ void EventLoopManagerWindows::register_notifier(Notifier& notifier) void EventLoopManagerWindows::unregister_notifier(Notifier& notifier) { // remove_first_matching would be clearer, but currently there is no such method in HashMap - ThreadData::the().notifiers.remove_all_matching([&](auto&, auto value) { return value == ¬ifier; }); + if (ThreadData::the()) + ThreadData::the()->notifiers.remove_all_matching([&](auto&, auto value) { return value == ¬ifier; }); } intptr_t EventLoopManagerWindows::register_timer(EventReceiver& object, int milliseconds, bool should_reload, TimerShouldFireWhenNotVisible fire_when_not_visible) @@ -190,15 +193,16 @@ intptr_t EventLoopManagerWindows::register_timer(EventReceiver& object, int mill BOOL rc = SetWaitableTimer(timer, &first_time, should_reload ? milliseconds : 0, NULL, NULL, FALSE); VERIFY(rc); - auto& timers = ThreadData::the().timers; + auto& timers = ThreadData::the()->timers; VERIFY(!timers.get(timer).has_value()); timers.set(Handle(timer), { object, fire_when_not_visible }); - return (intptr_t)timer; + return reinterpret_cast(timer); } void EventLoopManagerWindows::unregister_timer(intptr_t timer_id) { - ThreadData::the().timers.remove((HANDLE)timer_id); + if (ThreadData::the()) + ThreadData::the()->timers.remove(reinterpret_cast(timer_id)); } int EventLoopManagerWindows::register_signal([[maybe_unused]] int signal_number, [[maybe_unused]] Function handler) From 5ea4d2645800d6108fc148f41467a2954b7c2f0d Mon Sep 17 00:00:00 2001 From: stasoid Date: Wed, 8 Jan 2025 11:53:32 +0500 Subject: [PATCH 40/83] LibCore: Don't wait in WaitForMultipleObjects if thread event queue has pending events in EventLoopImplementationWindows This matches the behavior of the Linux version: https://github.com/LadybirdBrowser/ladybird/blob/911cd4aefd0c/Libraries/LibCore/EventLoopImplementationUnix.cpp#L371 --- Libraries/LibCore/EventLoopImplementationWindows.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Libraries/LibCore/EventLoopImplementationWindows.cpp b/Libraries/LibCore/EventLoopImplementationWindows.cpp index 37afaf01dba..c6274e01ea8 100644 --- a/Libraries/LibCore/EventLoopImplementationWindows.cpp +++ b/Libraries/LibCore/EventLoopImplementationWindows.cpp @@ -112,7 +112,9 @@ size_t EventLoopImplementationWindows::pump(PumpMode) for (auto& entry : timers) event_handles.append(entry.key.handle); - DWORD result = WaitForMultipleObjects(event_count, event_handles.data(), FALSE, INFINITE); + bool has_pending_events = ThreadEventQueue::current().has_pending_events(); + int timeout = has_pending_events ? 0 : INFINITE; + DWORD result = WaitForMultipleObjects(event_count, event_handles.data(), FALSE, timeout); size_t index = result - WAIT_OBJECT_0; VERIFY(index < event_count); From 2bfed2e417ce80f3595dbcf7081d342c5476f95c Mon Sep 17 00:00:00 2001 From: stasoid Date: Wed, 8 Jan 2025 12:27:40 +0500 Subject: [PATCH 41/83] LibCore: Check for all events in EventLoopImplementationWindows::pump This fixes the problem when none of the timers or notifiers get executed if wake() is called frequently. Note that calling WaitForMultipleObjects repeatedly until it fails will not work because rapidly firing timer can get all the attention. That's why I check every event individually with WaitForSingleObject. This behavior matches EventLoopImplementationUnix. --- .../EventLoopImplementationWindows.cpp | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/Libraries/LibCore/EventLoopImplementationWindows.cpp b/Libraries/LibCore/EventLoopImplementationWindows.cpp index c6274e01ea8..21e5235e304 100644 --- a/Libraries/LibCore/EventLoopImplementationWindows.cpp +++ b/Libraries/LibCore/EventLoopImplementationWindows.cpp @@ -94,7 +94,8 @@ int EventLoopImplementationWindows::exec() size_t EventLoopImplementationWindows::pump(PumpMode) { - auto thread_data = ThreadData::the(); + auto& event_queue = ThreadEventQueue::current(); + auto* thread_data = ThreadData::the(); auto& notifiers = thread_data->notifiers; auto& timers = thread_data->timers; @@ -112,25 +113,34 @@ size_t EventLoopImplementationWindows::pump(PumpMode) for (auto& entry : timers) event_handles.append(entry.key.handle); - bool has_pending_events = ThreadEventQueue::current().has_pending_events(); + bool has_pending_events = event_queue.has_pending_events(); int timeout = has_pending_events ? 0 : INFINITE; DWORD result = WaitForMultipleObjects(event_count, event_handles.data(), FALSE, timeout); - size_t index = result - WAIT_OBJECT_0; - VERIFY(index < event_count); - - if (index != 0) { - if (index <= notifiers.size()) { - Notifier* notifier = *notifiers.get(event_handles[index]); - ThreadEventQueue::current().post_event(*notifier, make(notifier->fd(), notifier->type())); - } else { - auto& timer = *timers.get(event_handles[index]); - if (auto strong_owner = timer.owner.strong_ref()) - if (timer.fire_when_not_visible == TimerShouldFireWhenNotVisible::Yes || strong_owner->is_visible_for_timer_purposes()) - ThreadEventQueue::current().post_event(*strong_owner, make()); + if (result == WAIT_TIMEOUT) { + // FIXME: This verification sometimes fails with ERROR_INVALID_HANDLE, but when I check + // the handles they all seem to be valid. + // VERIFY(GetLastError() == ERROR_SUCCESS || GetLastError() == ERROR_IO_PENDING); + } else { + size_t const index = result - WAIT_OBJECT_0; + VERIFY(index < event_count); + // : 1 - skip wake event + for (size_t i = index ? index : 1; i < event_count; i++) { + // i == index already checked by WaitForMultipleObjects + if (i == index || WaitForSingleObject(event_handles[i], 0) == WAIT_OBJECT_0) { + if (i <= notifiers.size()) { + Notifier* notifier = *notifiers.get(event_handles[i]); + event_queue.post_event(*notifier, make(notifier->fd(), notifier->type())); + } else { + auto& timer = *timers.get(event_handles[i]); + if (auto strong_owner = timer.owner.strong_ref()) + if (timer.fire_when_not_visible == TimerShouldFireWhenNotVisible::Yes || strong_owner->is_visible_for_timer_purposes()) + event_queue.post_event(*strong_owner, make()); + } + } } } - return ThreadEventQueue::current().process(); + return event_queue.process(); } void EventLoopImplementationWindows::quit(int code) From 17fcbce32462d5131d655d1b0b02fb1a456794d7 Mon Sep 17 00:00:00 2001 From: stasoid Date: Wed, 19 Feb 2025 22:36:05 +0500 Subject: [PATCH 42/83] LibCore: Make single-shot timer objects manually reset on Windows This fixes a really nasty EventLoop bug which I debugged for 2 weeks. The spin_until([&]{return completed_tasks == total_tasks;}) in TraversableNavigable::check_if_unloading_is_canceled spins forever. Cause of the bug: check_if_unloading_is_canceled is called deferred check_if_unloading_is_canceled creates a task: queue_global_task(..., [&] { ... completed_tasks++; })); This task is never executed. queue_global_task calls TaskQueue::add void TaskQueue::add(task) { m_tasks.append(task); m_event_loop->schedule(); } void HTML::EventLoop::schedule() { if (!m_system_event_loop_timer) m_system_event_loop_timer = Timer::create_single_shot( 0, // delay [&] { process(); }); if (!m_system_event_loop_timer->is_active()) m_system_event_loop_timer->restart(); } EventLoop::process executes one task from task queue and calls schedule again if there are more tasks. So task processing relies on one single-shot zero-delay timer, m_system_event_loop_timer. Timers and other notification events are handled by Core::EventLoop and Core::ThreadEventQueue, these are different from HTML::EventLoop and HTML::TaskQueue mentioned above. check_if_unloading_is_canceled is called using deferred_invoke mechanism, different from m_system_event_loop_timer, see Navigable::navigate and Core::EventLoop::deferred_invoke. The core of the problem is that Core::EventLoop::pump is called again (from spin_until) after timer fired but before its handler is executed. In ThreadEventQueue::process events are moved into local variable before executing. The first of those events is check_if_unloading_is_canceled. One of the rest events is Web::HTML::EventLoop::process sheduled in EventLoop::schedule using m_system_event_loop_timer. When check_if_unloading_is_canceled calls queue_global_task its m_system_event_loop_timer is still active because Timer::timer_event was not yet called, so the timer is not restarted. But Timer::timer_event (and hence EventLoop::process) will never execute because check_if_unloading_is_canceled calls spin_until after queue_global_task, and EventLoop::process is no longer in event_queue.m_private->queued_events. By making a single-shot timer manually-reset we are allowing it to fire several times. So when spin_until is executed m_system_event_loop_timer is fired again. Not an ideal solution, but this is the best I could come up with. This commit makes the behavior match EventLoopImplUnix, in which single-shot timer can also fire several times. Adding event_queue.process(); at the start of pump like in EvtLoopImplQt doesn't fix the problem. Note: Timer::start calls EventReceiver::start_timer, which calls EventLoop::register_timer with should_reload always set to true (single-shot vs periodic are handled in Timer::timer_event instead), so I use static_cast(object).is_single_shot() instead of !should_reload. --- Libraries/LibCore/EventLoopImplementationWindows.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/LibCore/EventLoopImplementationWindows.cpp b/Libraries/LibCore/EventLoopImplementationWindows.cpp index 21e5235e304..2e38479c518 100644 --- a/Libraries/LibCore/EventLoopImplementationWindows.cpp +++ b/Libraries/LibCore/EventLoopImplementationWindows.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -196,7 +197,9 @@ void EventLoopManagerWindows::unregister_notifier(Notifier& notifier) intptr_t EventLoopManagerWindows::register_timer(EventReceiver& object, int milliseconds, bool should_reload, TimerShouldFireWhenNotVisible fire_when_not_visible) { VERIFY(milliseconds >= 0); - HANDLE timer = CreateWaitableTimer(NULL, FALSE, NULL); + // FIXME: This is a temporary fix for issue #3641 + bool manual_reset = static_cast(object).is_single_shot(); + HANDLE timer = CreateWaitableTimer(NULL, manual_reset, NULL); VERIFY(timer); LARGE_INTEGER first_time = {}; From beb11f04479d1fb4525e87f5b39351a2ec963df8 Mon Sep 17 00:00:00 2001 From: stasoid Date: Sat, 15 Feb 2025 13:22:43 +0500 Subject: [PATCH 43/83] RequestServer: Compile on Windows --- Services/RequestServer/ConnectionFromClient.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Services/RequestServer/ConnectionFromClient.cpp b/Services/RequestServer/ConnectionFromClient.cpp index 10392eb8d25..914aba69510 100644 --- a/Services/RequestServer/ConnectionFromClient.cpp +++ b/Services/RequestServer/ConnectionFromClient.cpp @@ -22,6 +22,10 @@ #include #include #include +#ifdef AK_OS_WINDOWS +// needed because curl.h includes winsock2.h +# include +#endif #include namespace RequestServer { @@ -318,7 +322,7 @@ Messages::RequestServer::InitTransportResponse ConnectionFromClient::init_transp Messages::RequestServer::ConnectNewClientResponse ConnectionFromClient::connect_new_client() { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC int socket_fds[2] {}; if (auto err = Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds); err.is_error()) { @@ -372,6 +376,12 @@ void ConnectionFromClient::set_dns_server(ByteString host_or_address, u16 port, default_resolver()->dns.reset_connection(); } +#ifdef AK_OS_WINDOWS +void ConnectionFromClient::start_request(i32, ByteString, URL::URL, HTTP::HeaderMap, ByteBuffer, Core::ProxyData) +{ + VERIFY(0 && "RequestServer::ConnectionFromClient::start_request is not implemented"); +} +#else void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL::URL url, HTTP::HeaderMap request_headers, ByteBuffer request_body, Core::ProxyData proxy_data) { auto host = url.serialized_host().to_byte_string(); @@ -496,6 +506,7 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL: m_active_requests.set(request_id, move(request)); }); } +#endif static Requests::NetworkError map_curl_code_to_network_error(CURLcode const& code) { From 5ff32fb0902c38fe28645acfe0822f58ccf210c8 Mon Sep 17 00:00:00 2001 From: stasoid Date: Sat, 15 Feb 2025 13:31:31 +0500 Subject: [PATCH 44/83] WebContent: Replace static_assert IPC fixmes with comments Fixes compilation errors on Windows --- Services/WebContent/WebDriverConnection.cpp | 2 +- Services/WebContent/main.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Services/WebContent/WebDriverConnection.cpp b/Services/WebContent/WebDriverConnection.cpp index 16f81ccacec..d6a90062ed8 100644 --- a/Services/WebContent/WebDriverConnection.cpp +++ b/Services/WebContent/WebDriverConnection.cpp @@ -188,7 +188,7 @@ static bool fire_an_event(FlyString const& name, Optional ta ErrorOr> WebDriverConnection::connect(Web::PageClient& page_client, ByteString const& webdriver_ipc_path) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC and Windows IPC dbgln_if(WEBDRIVER_DEBUG, "Trying to connect to {}", webdriver_ipc_path); auto socket = TRY(Core::LocalSocket::connect(webdriver_ipc_path)); diff --git a/Services/WebContent/main.cpp b/Services/WebContent/main.cpp index 0ae19e324b9..cabe2e7d512 100644 --- a/Services/WebContent/main.cpp +++ b/Services/WebContent/main.cpp @@ -210,7 +210,7 @@ ErrorOr serenity_main(Main::Arguments arguments) if (maybe_content_filter_error.is_error()) dbgln("Failed to load content filters: {}", maybe_content_filter_error.error()); - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto webcontent_socket = TRY(Core::take_over_socket_from_system_server("WebContent"sv)); auto webcontent_client = TRY(WebContent::ConnectionFromClient::try_create(make(move(webcontent_socket)))); @@ -250,7 +250,7 @@ static ErrorOr load_content_filters(StringView config_path) ErrorOr initialize_resource_loader(GC::Heap& heap, int request_server_socket) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto socket = TRY(Core::LocalSocket::adopt_fd(request_server_socket)); TRY(socket->set_blocking(true)); @@ -267,7 +267,7 @@ ErrorOr initialize_resource_loader(GC::Heap& heap, int request_server_sock ErrorOr initialize_image_decoder(int image_decoder_socket) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto socket = TRY(Core::LocalSocket::adopt_fd(image_decoder_socket)); TRY(socket->set_blocking(true)); @@ -284,7 +284,7 @@ ErrorOr initialize_image_decoder(int image_decoder_socket) ErrorOr reinitialize_image_decoder(IPC::File const& image_decoder_socket) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto socket = TRY(Core::LocalSocket::adopt_fd(image_decoder_socket.take_fd())); TRY(socket->set_blocking(true)); From 32ddeb82d6ad9ced787a11a7b1fc8b5852baa7bb Mon Sep 17 00:00:00 2001 From: stasoid Date: Fri, 14 Feb 2025 15:31:43 +0500 Subject: [PATCH 45/83] LibURL+LibWeb: Remove leading slash when converting url to path ...on Windows --- Libraries/LibURL/URL.cpp | 12 ++++++++++++ Libraries/LibURL/URL.h | 1 + Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp | 2 +- Libraries/LibWeb/Loader/Resource.cpp | 2 +- Libraries/LibWeb/Loader/ResourceLoader.cpp | 6 +++--- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Libraries/LibURL/URL.cpp b/Libraries/LibURL/URL.cpp index ec7932199e6..8a82f790854 100644 --- a/Libraries/LibURL/URL.cpp +++ b/Libraries/LibURL/URL.cpp @@ -246,6 +246,18 @@ String URL::serialize_path() const return output.to_string_without_validation(); } +// This function is used whenever a path is needed to access the actual file on disk. +// On Windows serialize_path can produce a path like /C:/path/to/tst.htm, so the leading slash needs to be removed to obtain a valid path. +ByteString URL::file_path() const +{ + ByteString path = percent_decode(serialize_path()); +#ifdef AK_OS_WINDOWS + if (path.starts_with('/')) + path = path.substring(1); +#endif + return path; +} + // https://url.spec.whatwg.org/#concept-url-serializer String URL::serialize(ExcludeFragment exclude_fragment) const { diff --git a/Libraries/LibURL/URL.h b/Libraries/LibURL/URL.h index ad5e60f8bd7..88ec9f965a8 100644 --- a/Libraries/LibURL/URL.h +++ b/Libraries/LibURL/URL.h @@ -122,6 +122,7 @@ public: } String serialize_path() const; + ByteString file_path() const; String serialize(ExcludeFragment = ExcludeFragment::No) const; ByteString serialize_for_display() const; ByteString to_byte_string() const { return serialize().to_byte_string(); } diff --git a/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp b/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp index 5f2fce950c3..d256b698037 100644 --- a/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp +++ b/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp @@ -45,7 +45,7 @@ ErrorOr load_error_page(URL::URL const& url, StringView error_message) ErrorOr load_file_directory_page(URL::URL const& url) { // Generate HTML contents entries table - auto lexical_path = LexicalPath(URL::percent_decode(url.serialize_path())); + auto lexical_path = LexicalPath(url.file_path()); Core::DirIterator dt(lexical_path.string(), Core::DirIterator::Flags::SkipParentAndBaseDir); Vector names; while (dt.has_next()) diff --git a/Libraries/LibWeb/Loader/Resource.cpp b/Libraries/LibWeb/Loader/Resource.cpp index 075cd7e378f..3b2bf088e6f 100644 --- a/Libraries/LibWeb/Loader/Resource.cpp +++ b/Libraries/LibWeb/Loader/Resource.cpp @@ -102,7 +102,7 @@ void Resource::did_load(Badge, ReadonlyBytes data, HTTP::HeaderM if (content_type_options.value_or("").equals_ignoring_ascii_case("nosniff"sv)) { m_mime_type = "text/plain"; } else { - m_mime_type = Core::guess_mime_type_based_on_filename(URL::percent_decode(url().serialize_path())); + m_mime_type = Core::guess_mime_type_based_on_filename(url().file_path()); } } diff --git a/Libraries/LibWeb/Loader/ResourceLoader.cpp b/Libraries/LibWeb/Loader/ResourceLoader.cpp index 08fd211fb67..6b417ea5262 100644 --- a/Libraries/LibWeb/Loader/ResourceLoader.cpp +++ b/Libraries/LibWeb/Loader/ResourceLoader.cpp @@ -321,7 +321,7 @@ void ResourceLoader::load(LoadRequest& request, GC::Root succes } auto data = resource.value()->data(); - auto response_headers = response_headers_for_file(URL::percent_decode(url.serialize_path()), resource.value()->modified_time()); + auto response_headers = response_headers_for_file(url.file_path(), resource.value()->modified_time()); // FIXME: Implement timing info for resource requests. Requests::RequestTimingInfo fixme_implement_timing_info {}; @@ -339,7 +339,7 @@ void ResourceLoader::load(LoadRequest& request, GC::Root succes return; } - FileRequest file_request(URL::percent_decode(url.serialize_path()), [this, success_callback, error_callback, request, respond_directory_page](ErrorOr file_or_error) { + FileRequest file_request(url.file_path(), [this, success_callback, error_callback, request, respond_directory_page](ErrorOr file_or_error) { --m_pending_loads; if (on_load_counter_change) on_load_counter_change(); @@ -387,7 +387,7 @@ void ResourceLoader::load(LoadRequest& request, GC::Root succes } auto data = maybe_data.release_value(); - auto response_headers = response_headers_for_file(URL::percent_decode(request.url().serialize_path()), st_or_error.value().st_mtime); + auto response_headers = response_headers_for_file(request.url().file_path(), st_or_error.value().st_mtime); // FIXME: Implement timing info for file requests. Requests::RequestTimingInfo fixme_implement_timing_info {}; From 7c3de67b16958ba330758de930359bac4fea4a62 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 22:51:11 +0200 Subject: [PATCH 46/83] LibWeb/IDB: Dont set the forced flag when aborting connection --- Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 6b1f81c648d..bff6fe8f6dd 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -149,14 +149,13 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& auto upgrade_transaction = upgrade_a_database(realm, connection, version, request); // 7. If connection was closed, return a newly created "AbortError" DOMException and abort these steps. - if (connection->state() == IDBDatabase::ConnectionState::Closed) { + if (connection->state() == IDBDatabase::ConnectionState::Closed) return WebIDL::AbortError::create(realm, "Connection was closed"_string); - } // 8. If the upgrade transaction was aborted, run the steps to close a database connection with connection, // return a newly created "AbortError" DOMException and abort these steps. if (upgrade_transaction->aborted()) { - close_a_database_connection(*connection, true); + close_a_database_connection(*connection); return WebIDL::AbortError::create(realm, "Upgrade transaction was aborted"_string); } } From d1dabb903979c02c643505bb23b1af7f5a4ed4a0 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 22:53:43 +0200 Subject: [PATCH 47/83] LibWeb/IDB: Make close_a_database_connection take a GC::Ref --- Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp | 8 ++++---- Libraries/LibWeb/IndexedDB/Internal/Algorithms.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index bff6fe8f6dd..e51d4822afb 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -313,18 +313,18 @@ ErrorOr> convert_a_value_to_a_key(JS::Realm& realm, JS::Value input } // https://w3c.github.io/IndexedDB/#close-a-database-connection -void close_a_database_connection(IDBDatabase& connection, bool forced) +void close_a_database_connection(GC::Ref connection, bool forced) { // 1. Set connection’s close pending flag to true. - connection.set_close_pending(true); + connection->set_close_pending(true); // FIXME: 2. If the forced flag is true, then for each transaction created using connection run abort a transaction with transaction and newly created "AbortError" DOMException. // FIXME: 3. Wait for all transactions created using connection to complete. Once they are complete, connection is closed. - connection.set_state(IDBDatabase::ConnectionState::Closed); + connection->set_state(IDBDatabase::ConnectionState::Closed); // 4. If the forced flag is true, then fire an event named close at connection. if (forced) - connection.dispatch_event(DOM::Event::create(connection.realm(), HTML::EventNames::close)); + connection->dispatch_event(DOM::Event::create(connection->realm(), HTML::EventNames::close)); } // https://w3c.github.io/IndexedDB/#upgrade-a-database diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h index 907c2d1924b..09a33be3f48 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h @@ -20,7 +20,7 @@ using KeyPath = Variant>; WebIDL::ExceptionOr> open_a_database_connection(JS::Realm&, StorageAPI::StorageKey, String, Optional, GC::Ref); bool fire_a_version_change_event(JS::Realm&, FlyString const&, GC::Ref, u64, Optional); ErrorOr> convert_a_value_to_a_key(JS::Realm&, JS::Value, Vector = {}); -void close_a_database_connection(IDBDatabase&, bool forced = false); +void close_a_database_connection(GC::Ref, bool forced = false); GC::Ref upgrade_a_database(JS::Realm&, GC::Ref, u64, GC::Ref); WebIDL::ExceptionOr delete_a_database(JS::Realm&, StorageAPI::StorageKey, String, GC::Ref); void abort_a_transaction(GC::Ref, GC::Ptr); From fc93ec135ef1f6f2d8467cb0bc09d7f1f22dd2c8 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 22:59:07 +0200 Subject: [PATCH 48/83] LibWeb/IDB: Keep track of the connection used to start a transaction --- Libraries/LibWeb/IndexedDB/IDBDatabase.cpp | 1 + Libraries/LibWeb/IndexedDB/IDBDatabase.h | 6 ++++++ Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 1 + 3 files changed, 8 insertions(+) diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp index 781136c3b35..a527609be87 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp @@ -44,6 +44,7 @@ void IDBDatabase::visit_edges(Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_object_store_set); visitor.visit(m_associated_database); + visitor.visit(m_transactions); } void IDBDatabase::set_onabort(WebIDL::CallbackType* event_handler) diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.h b/Libraries/LibWeb/IndexedDB/IDBDatabase.h index d2127fcfc97..73578505ca3 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.h +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.h @@ -59,6 +59,9 @@ public: m_object_store_set.remove_first_matching([&](auto& entry) { return entry == object_store; }); } + [[nodiscard]] ReadonlySpan> transactions() { return m_transactions; } + void add_transaction(GC::Ref transaction) { m_transactions.append(transaction); } + [[nodiscard]] GC::Ref object_store_names(); WebIDL::ExceptionOr> create_object_store(String const&, IDBObjectStoreParameters const&); WebIDL::ExceptionOr delete_object_store(String const&); @@ -98,6 +101,9 @@ private: // So we stash the one we have when opening a connection. GC::Ref m_associated_database; + // NOTE: We need to keep track of what transactions were created by this connection + Vector> m_transactions; + // NOTE: Used for debug purposes String m_uuid; }; diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index a1d56e322cf..7cca446090f 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -24,6 +24,7 @@ IDBTransaction::IDBTransaction(JS::Realm& realm, GC::Ref connection , m_scope(move(scopes)) { m_uuid = MUST(Crypto::generate_random_uuid()); + connection->add_transaction(*this); } GC::Ref IDBTransaction::create(JS::Realm& realm, GC::Ref connection, Bindings::IDBTransactionMode mode, Bindings::IDBTransactionDurability durability = Bindings::IDBTransactionDurability::Default, Vector> scopes = {}) From b6b00acbd13edf811e6467c03bbf6894c70a9ceb Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 22:59:34 +0200 Subject: [PATCH 49/83] LibWeb/IDB: Implement abort and wait steps for closing a connection --- Libraries/LibWeb/IndexedDB/IDBTransaction.h | 1 + .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index 5ca6ee15325..194d40c7023 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -56,6 +56,7 @@ public: [[nodiscard]] bool is_upgrade_transaction() const { return m_mode == Bindings::IDBTransactionMode::Versionchange; } [[nodiscard]] bool is_readonly() const { return m_mode == Bindings::IDBTransactionMode::Readonly; } [[nodiscard]] bool is_readwrite() const { return m_mode == Bindings::IDBTransactionMode::Readwrite; } + [[nodiscard]] bool is_finished() const { return m_state == TransactionState::Finished; } WebIDL::ExceptionOr abort(); diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index e51d4822afb..abfe6d31b8d 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -315,16 +315,41 @@ ErrorOr> convert_a_value_to_a_key(JS::Realm& realm, JS::Value input // https://w3c.github.io/IndexedDB/#close-a-database-connection void close_a_database_connection(GC::Ref connection, bool forced) { + auto& realm = connection->realm(); + // 1. Set connection’s close pending flag to true. connection->set_close_pending(true); - // FIXME: 2. If the forced flag is true, then for each transaction created using connection run abort a transaction with transaction and newly created "AbortError" DOMException. - // FIXME: 3. Wait for all transactions created using connection to complete. Once they are complete, connection is closed. + // 2. If the forced flag is true, then for each transaction created using connection run abort a transaction with transaction and newly created "AbortError" DOMException. + if (forced) { + for (auto const& transaction : connection->transactions()) { + abort_a_transaction(*transaction, WebIDL::AbortError::create(realm, "Connection was closed"_string)); + } + } + + // 3. Wait for all transactions created using connection to complete. Once they are complete, connection is closed. + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [connection]() { + if constexpr (IDB_DEBUG) { + dbgln("close_a_database_connection: waiting for step 3"); + dbgln("transactions created using connection:"); + for (auto const& transaction : connection->transactions()) { + dbgln(" - {} - {}", transaction->uuid(), (u8)transaction->state()); + } + } + + for (auto const& transaction : connection->transactions()) { + if (!transaction->is_finished()) + return false; + } + + return true; + })); + connection->set_state(IDBDatabase::ConnectionState::Closed); // 4. If the forced flag is true, then fire an event named close at connection. if (forced) - connection->dispatch_event(DOM::Event::create(connection->realm(), HTML::EventNames::close)); + connection->dispatch_event(DOM::Event::create(realm, HTML::EventNames::close)); } // https://w3c.github.io/IndexedDB/#upgrade-a-database From da56c1b1eba89544c580be164eb4742624d69931 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 23:11:12 +0200 Subject: [PATCH 50/83] LibWeb/IDB: Implement IDBTransaction::commit --- Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 15 +++++ Libraries/LibWeb/IndexedDB/IDBTransaction.h | 2 + Libraries/LibWeb/IndexedDB/IDBTransaction.idl | 2 +- .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 58 +++++++++++++++++++ .../LibWeb/IndexedDB/Internal/Algorithms.h | 1 + 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index 7cca446090f..6e58b3ba56c 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -103,4 +103,19 @@ GC::Ref IDBTransaction::object_store_names() return create_a_sorted_name_list(realm(), names); } +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit +WebIDL::ExceptionOr IDBTransaction::commit() +{ + auto& realm = this->realm(); + + // 1. If this's state is not active, then throw an "InvalidStateError" DOMException. + if (m_state != TransactionState::Active) + return WebIDL::InvalidStateError::create(realm, "Transaction is not active while commiting"_string); + + // 2. Run commit a transaction with this. + commit_a_transaction(realm, *this); + + return {}; +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index 194d40c7023..bc5af18a485 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -44,6 +44,7 @@ public: [[nodiscard]] GC::Ptr associated_request() const { return m_associated_request; } [[nodiscard]] bool aborted() const { return m_aborted; } [[nodiscard]] GC::Ref object_store_names(); + [[nodiscard]] RequestList& request_list() { return m_request_list; } [[nodiscard]] ReadonlySpan> scope() const { return m_scope; } [[nodiscard]] String uuid() const { return m_uuid; } @@ -59,6 +60,7 @@ public: [[nodiscard]] bool is_finished() const { return m_state == TransactionState::Finished; } WebIDL::ExceptionOr abort(); + WebIDL::ExceptionOr commit(); void set_onabort(WebIDL::CallbackType*); WebIDL::CallbackType* onabort(); diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl index c343a328ab3..667d36a6cdd 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl @@ -10,7 +10,7 @@ interface IDBTransaction : EventTarget { [SameObject, ImplementedAs=connection] readonly attribute IDBDatabase db; readonly attribute DOMException? error; [FIXME] IDBObjectStore objectStore(DOMString name); - [FIXME] undefined commit(); + undefined commit(); undefined abort(); attribute EventHandler onabort; diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index abfe6d31b8d..4b325a9fab8 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -706,4 +708,60 @@ GC::Ref create_a_sorted_name_list(JS::Realm& realm, Vector< return HTML::DOMStringList::create(realm, names); } +// https://w3c.github.io/IndexedDB/#commit-a-transaction +void commit_a_transaction(JS::Realm& realm, GC::Ref transaction) +{ + // 1. Set transaction’s state to committing. + transaction->set_state(IDBTransaction::TransactionState::Committing); + + dbgln_if(IDB_DEBUG, "commit_a_transaction: transaction {} is committing", transaction->uuid()); + + // 2. Run the following steps in parallel: + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, transaction]() { + HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + + // 1. Wait until every item in transaction’s request list is processed. + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [transaction]() { + if constexpr (IDB_DEBUG) { + dbgln("commit_a_transaction: waiting for step 1"); + dbgln("requests in queue:"); + for (auto const& request : transaction->request_list()) { + dbgln(" - {} = {}", request->uuid(), request->processed() ? "processed"sv : "not processed"sv); + } + } + + return transaction->request_list().all_requests_processed(); + })); + + // 2. If transaction’s state is no longer committing, then terminate these steps. + if (transaction->state() != IDBTransaction::TransactionState::Committing) + return; + + // FIXME: 3. Attempt to write any outstanding changes made by transaction to the database, considering transaction’s durability hint. + // FIXME: 4. If an error occurs while writing the changes to the database, then run abort a transaction with transaction and an appropriate type for the error, for example "QuotaExceededError" or "UnknownError" DOMException, and terminate these steps. + + // 5. Queue a task to run these steps: + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction->realm().vm().heap(), [transaction]() { + // 1. If transaction is an upgrade transaction, then set transaction’s connection's associated database's upgrade transaction to null. + if (transaction->is_upgrade_transaction()) + transaction->connection()->associated_database()->set_upgrade_transaction(nullptr); + + // 2. Set transaction’s state to finished. + transaction->set_state(IDBTransaction::TransactionState::Finished); + + // 3. Fire an event named complete at transaction. + transaction->dispatch_event(DOM::Event::create(transaction->realm(), HTML::EventNames::complete)); + + // 4. If transaction is an upgrade transaction, then let request be the request associated with transaction and set request’s transaction to null. + if (transaction->is_upgrade_transaction()) { + auto request = transaction->associated_request(); + request->set_transaction(nullptr); + + // Ad-hoc: Clear the two-way binding. + transaction->set_associated_request(nullptr); + } + })); + })); +} + } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h index 09a33be3f48..7d7540e5fe7 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h @@ -27,5 +27,6 @@ void abort_a_transaction(GC::Ref, GC::Ptr) JS::Value convert_a_key_to_a_value(JS::Realm&, GC::Ref); bool is_valid_key_path(KeyPath const&); GC::Ref create_a_sorted_name_list(JS::Realm&, Vector); +void commit_a_transaction(JS::Realm&, GC::Ref); } From de640ffef47d4548d52ad82b039b26105d6f4c9f Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 23:14:10 +0200 Subject: [PATCH 51/83] LibWeb/IDB: Implement auto-commit for IDBTransaction --- Libraries/LibWeb/IndexedDB/IDBTransaction.h | 2 +- Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index bc5af18a485..3913d1c506e 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -49,10 +49,10 @@ public: [[nodiscard]] String uuid() const { return m_uuid; } void set_mode(Bindings::IDBTransactionMode mode) { m_mode = mode; } - void set_state(TransactionState state) { m_state = state; } void set_error(GC::Ptr error) { m_error = error; } void set_associated_request(GC::Ptr request) { m_associated_request = request; } void set_aborted(bool aborted) { m_aborted = aborted; } + void set_state(TransactionState state) { m_state = state; } [[nodiscard]] bool is_upgrade_transaction() const { return m_mode == Bindings::IDBTransactionMode::Versionchange; } [[nodiscard]] bool is_readonly() const { return m_mode == Bindings::IDBTransactionMode::Readonly; } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 4b325a9fab8..61ee5b3c7e7 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -409,6 +409,15 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refstate() == IDBTransaction::TransactionState::Inactive && transaction->request_list().is_empty() && !transaction->aborted()) + commit_a_transaction(realm, transaction); + wait_for_transaction = false; })); From a61315a68e5e406eb2789bd756b9bf73be2d5d6f Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 23:15:39 +0200 Subject: [PATCH 52/83] LibWeb/IDB: Use correct wait condition when upgrading database --- Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 61ee5b3c7e7..6ed7f2060da 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -383,8 +383,7 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refset_processed(true); // 10. Queue a task to run these steps: - IGNORE_USE_IN_ESCAPING_LAMBDA bool wait_for_transaction = true; - HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(realm.vm().heap(), [&realm, request, connection, transaction, old_version, version, &wait_for_transaction]() { + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(realm.vm().heap(), [&realm, request, connection, transaction, old_version, version]() { // 1. Set request’s result to connection. request->set_result(connection); @@ -417,14 +416,12 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refstate() == IDBTransaction::TransactionState::Inactive && transaction->request_list().is_empty() && !transaction->aborted()) commit_a_transaction(realm, transaction); - - wait_for_transaction = false; })); // 11. Wait for transaction to finish. - HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [&wait_for_transaction]() { + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [transaction]() { dbgln_if(IDB_DEBUG, "upgrade_a_database: waiting for step 11"); - return !wait_for_transaction; + return transaction->is_finished(); })); return transaction; From 8fcb54dadac55a108c594f0358d3458ccf149f70 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 23:18:18 +0200 Subject: [PATCH 53/83] LibWeb/IDB: Abort requests in the transactions request list --- .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 6ed7f2060da..aa37c794520 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -547,12 +547,28 @@ void abort_a_transaction(GC::Ref transaction, GC::Ptrset_error(error); - // FIXME: 5. For each request of transaction’s request list, abort the steps to asynchronously execute a request for request, - // set request’s processed flag to true, and queue a task to run these steps: - // FIXME: 1. Set request’s done flag to true. - // FIXME: 2. Set request’s result to undefined. - // FIXME: 3. Set request’s error to a newly created "AbortError" DOMException. - // FIXME: 4. Fire an event named error at request with its bubbles and cancelable attributes initialized to true. + // 5. For each request of transaction’s request list, + for (auto const& request : transaction->request_list()) { + // FIXME: abort the steps to asynchronously execute a request for request, + + // set request’s processed flag to true + request->set_processed(true); + + // and queue a task to run these steps: + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction->realm().vm().heap(), [request]() { + // 1. Set request’s done flag to true. + request->set_done(true); + + // 2. Set request’s result to undefined. + request->set_result(JS::js_undefined()); + + // 3. Set request’s error to a newly created "AbortError" DOMException. + request->set_error(WebIDL::AbortError::create(request->realm(), "Transaction was aborted"_string)); + + // 4. Fire an event named error at request with its bubbles and cancelable attributes initialized to true. + request->dispatch_event(DOM::Event::create(request->realm(), HTML::EventNames::error, { .bubbles = true, .cancelable = true })); + })); + } // 6. Queue a task to run these steps: HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction->realm().vm().heap(), [transaction]() { From fc06d088c3cc87253340e570188bb3be5b41abed Mon Sep 17 00:00:00 2001 From: stelar7 Date: Wed, 9 Apr 2025 23:24:44 +0200 Subject: [PATCH 54/83] LibWeb/IDB: Implement IDBTransaction::objectStore --- Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 29 +++++++++++++++++++ Libraries/LibWeb/IndexedDB/IDBTransaction.h | 3 ++ Libraries/LibWeb/IndexedDB/IDBTransaction.idl | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index 6e58b3ba56c..d73e6bd128c 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -118,4 +119,32 @@ WebIDL::ExceptionOr IDBTransaction::commit() return {}; } +GC::Ptr IDBTransaction::object_store_named(String const& name) const +{ + for (auto const& store : m_scope) { + if (store->name() == name) + return store; + } + + return nullptr; +} + +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore +WebIDL::ExceptionOr> IDBTransaction::object_store(String const& name) +{ + auto& realm = this->realm(); + + // 1. If this's state is finished, then throw an "InvalidStateError" DOMException. + if (m_state == TransactionState::Finished) + return WebIDL::InvalidStateError::create(realm, "Transaction is finished"_string); + + // 2. Let store be the object store named name in this's scope, or throw a "NotFoundError" DOMException if none. + auto store = object_store_named(name); + if (!store) + return WebIDL::NotFoundError::create(realm, "Object store not found"_string); + + // 3. Return an object store handle associated with store and this. + return IDBObjectStore::create(realm, *store, *this); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index 3913d1c506e..1894771363a 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -59,8 +59,11 @@ public: [[nodiscard]] bool is_readwrite() const { return m_mode == Bindings::IDBTransactionMode::Readwrite; } [[nodiscard]] bool is_finished() const { return m_state == TransactionState::Finished; } + GC::Ptr object_store_named(String const& name) const; + WebIDL::ExceptionOr abort(); WebIDL::ExceptionOr commit(); + WebIDL::ExceptionOr> object_store(String const& name); void set_onabort(WebIDL::CallbackType*); WebIDL::CallbackType* onabort(); diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl index 667d36a6cdd..d31e70bf604 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl @@ -9,7 +9,7 @@ interface IDBTransaction : EventTarget { readonly attribute IDBTransactionDurability durability; [SameObject, ImplementedAs=connection] readonly attribute IDBDatabase db; readonly attribute DOMException? error; - [FIXME] IDBObjectStore objectStore(DOMString name); + IDBObjectStore objectStore(DOMString name); undefined commit(); undefined abort(); From aa4e303b9f7512c6fa8dbf443a47b3e3dd641ef7 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Thu, 10 Apr 2025 00:25:13 +0200 Subject: [PATCH 55/83] LibWeb/IDB: Make some debug messages more descriptive --- Libraries/LibWeb/IndexedDB/IDBDatabase.cpp | 2 +- Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp index a527609be87..2ff917b6179 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp @@ -176,7 +176,7 @@ WebIDL::ExceptionOr IDBDatabase::delete_object_store(String const& name) // 4. Let store be the object store named name in database, or throw a "NotFoundError" DOMException if none. auto store = database->object_store_with_name(name); if (!store) - return WebIDL::NotFoundError::create(realm, "Object store not found"_string); + return WebIDL::NotFoundError::create(realm, "Object store not found while trying to delete"_string); // 5. Remove store from this's object store set. this->remove_from_object_store_set(*store); diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index d73e6bd128c..6dc8ffcfccf 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -141,7 +141,7 @@ WebIDL::ExceptionOr> IDBTransaction::object_store(String // 2. Let store be the object store named name in this's scope, or throw a "NotFoundError" DOMException if none. auto store = object_store_named(name); if (!store) - return WebIDL::NotFoundError::create(realm, "Object store not found"_string); + return WebIDL::NotFoundError::create(realm, "Object store not found in transactions scope"_string); // 3. Return an object store handle associated with store and this. return IDBObjectStore::create(realm, *store, *this); From 0a298dba27d6ae51e7a0c115c5ee4a4625df17b8 Mon Sep 17 00:00:00 2001 From: stelar7 Date: Thu, 10 Apr 2025 00:33:05 +0200 Subject: [PATCH 56/83] LibWeb/IDB: Dont go back to inactive if we finished during upgrade --- .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index aa37c794520..bcc208cac5e 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -401,21 +401,25 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refset_state(IDBTransaction::TransactionState::Inactive); + // AD-HOC: If the transaction was aborted by the event, then DONT set the transaction back to inactive. + // https://github.com/w3c/IndexedDB/issues/436#issuecomment-2791113467 + if (transaction->state() != IDBTransaction::TransactionState::Finished) { - // 7. If didThrow is true, run abort a transaction with transaction and a newly created "AbortError" DOMException. - if (did_throw) - abort_a_transaction(*transaction, WebIDL::AbortError::create(realm, "Version change event threw an exception"_string)); + // 6. Set transaction’s state to inactive. + transaction->set_state(IDBTransaction::TransactionState::Inactive); - // AD-HOC: - // https://github.com/w3c/IndexedDB/issues/436 - // The implementation must attempt to commit a transaction when all requests placed against the transaction have completed - // and their returned results handled, - // no new requests have been placed against the transaction, - // and the transaction has not been aborted. - if (transaction->state() == IDBTransaction::TransactionState::Inactive && transaction->request_list().is_empty() && !transaction->aborted()) - commit_a_transaction(realm, transaction); + // 7. If didThrow is true, run abort a transaction with transaction and a newly created "AbortError" DOMException. + if (did_throw) + abort_a_transaction(transaction, WebIDL::AbortError::create(realm, "Version change event threw an exception"_string)); + + // AD-HOC: + // The implementation must attempt to commit a transaction when all requests placed against the transaction have completed + // and their returned results handled, + // no new requests have been placed against the transaction, + // and the transaction has not been aborted. + if (transaction->state() == IDBTransaction::TransactionState::Inactive && transaction->request_list().is_empty() && !transaction->aborted()) + commit_a_transaction(realm, transaction); + } })); // 11. Wait for transaction to finish. From 4a6998497f479ce06fe920f9bc013ff7435f0942 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 10 Apr 2025 12:42:34 +0200 Subject: [PATCH 57/83] LibWeb: Don't recalculate margin box rect for preceding floats We already stored that rect while building up the side data during floating box layout. No functional changes. --- Libraries/LibWeb/Layout/BlockFormattingContext.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp index aa93dbbbfcc..7c52c6ca8f8 100644 --- a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp @@ -1089,8 +1089,7 @@ void BlockFormattingContext::layout_floating_box(Box const& box, BlockContainer // Walk all currently tracked floats on the side we're floating towards. // We're looking for the innermost preceding float that intersects vertically with `box`. for (auto& preceding_float : side_data.current_boxes.in_reverse()) { - auto const preceding_float_rect = margin_box_rect_in_ancestor_coordinate_space(preceding_float.used_values, root()); - if (!preceding_float_rect.contains_vertically(y_in_root)) + if (!preceding_float.margin_box_rect_in_root_coordinate_space.contains_vertically(y_in_root)) continue; // We found a preceding float that intersects vertically with the current float. // Now we need to find out if there's enough inline-axis space to stack them next to each other. From 76105d6a020bb174617d46b4c26edd67977015d5 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 10 Apr 2025 13:43:58 +0200 Subject: [PATCH 58/83] LibWeb: Use `LayoutState::set_content_x/y()` where possible No functional changes. --- Libraries/LibWeb/Layout/BlockFormattingContext.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp index 7c52c6ca8f8..c302731501f 100644 --- a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp @@ -95,7 +95,7 @@ void BlockFormattingContext::run(AvailableSpace const& available_space) // FIXME: this should take writing modes into consideration. auto legend_height = legend_state.border_box_height(); auto new_y = -((legend_height) / 2) - fieldset_state.padding_top; - legend_state.set_content_offset({ legend_state.offset.x(), new_y }); + legend_state.set_content_y(new_y); // If the computed value of 'inline-size' is 'auto', // then the used value is the fit-content inline size. @@ -955,7 +955,7 @@ void BlockFormattingContext::place_block_level_element_in_normal_flow_vertically { auto& box_state = m_state.get_mutable(child_box); y += box_state.border_box_top(); - box_state.set_content_offset(CSSPixelPoint { box_state.offset.x(), y }); + box_state.set_content_y(y); for (auto const& float_box : m_left_floats.all_boxes) float_box->margin_box_rect_in_root_coordinate_space = margin_box_rect_in_ancestor_coordinate_space(float_box->used_values, root()); @@ -1004,7 +1004,7 @@ void BlockFormattingContext::place_block_level_element_in_normal_flow_horizontal x += box_state.margin_box_left(); } - box_state.set_content_offset({ x, box_state.offset.y() }); + box_state.set_content_x(x); } void BlockFormattingContext::layout_viewport(AvailableSpace const& available_space) From 8257788a20daf223a047a0a7ba91e9dd30dc1399 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 10 Apr 2025 13:36:15 +0200 Subject: [PATCH 59/83] LibWeb: Ignore negative margins for margin box rect Negative margins are processed through the `offset` in layout state, and should not contribute to the margin box' rect's size or position. Fixes #4249. --- Libraries/LibWeb/Layout/FormattingContext.cpp | 8 ++++---- .../float-with-negative-margins.txt | 13 +++++++++++++ .../float-with-negative-margins.html | 13 +++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt create mode 100644 Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html diff --git a/Libraries/LibWeb/Layout/FormattingContext.cpp b/Libraries/LibWeb/Layout/FormattingContext.cpp index 9424150d6a6..b7d3882890e 100644 --- a/Libraries/LibWeb/Layout/FormattingContext.cpp +++ b/Libraries/LibWeb/Layout/FormattingContext.cpp @@ -1828,12 +1828,12 @@ CSSPixels FormattingContext::box_baseline(Box const& box) const { return { { - -used_values.margin_box_left(), - -used_values.margin_box_top(), + -max(used_values.margin_box_left(), 0), + -max(used_values.margin_box_top(), 0), }, { - used_values.margin_box_left() + used_values.content_width() + used_values.margin_box_right(), - used_values.margin_box_top() + used_values.content_height() + used_values.margin_box_bottom(), + max(used_values.margin_box_left(), 0) + used_values.content_width() + max(used_values.margin_box_right(), 0), + max(used_values.margin_box_top(), 0) + used_values.content_height() + max(used_values.margin_box_bottom(), 0), }, }; } diff --git a/Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt b/Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt new file mode 100644 index 00000000000..a7d359c32eb --- /dev/null +++ b/Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt @@ -0,0 +1,13 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x48 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x0 children: inline + BlockContainer at (8,-2) content-size 50x50 floating [BFC] children: not-inline + TextNode <#text> + BlockContainer at (58,-2) content-size 50x50 floating [BFC] children: not-inline + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x48] + PaintableWithLines (BlockContainer) [8,8 784x0] + PaintableWithLines (BlockContainer
.r) [8,-2 50x50] + PaintableWithLines (BlockContainer
.g) [58,-2 50x50] diff --git a/Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html b/Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html new file mode 100644 index 00000000000..53b5002a35a --- /dev/null +++ b/Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html @@ -0,0 +1,13 @@ + + +
+
From b3980d40f76ea4efac27ad6cc20eb2388f58e732 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Fri, 11 Apr 2025 11:09:10 +0100 Subject: [PATCH 60/83] LibWeb: Round to the nearest integer when interpolating integer values --- Libraries/LibWeb/CSS/Interpolation.cpp | 9 +- .../animations/z-index-interpolation.txt | 256 ++++++++++++++++++ .../animations/z-index-interpolation.html | 130 +++++++++ 3 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html diff --git a/Libraries/LibWeb/CSS/Interpolation.cpp b/Libraries/LibWeb/CSS/Interpolation.cpp index de84c8345d8..282c2795c6f 100644 --- a/Libraries/LibWeb/CSS/Interpolation.cpp +++ b/Libraries/LibWeb/CSS/Interpolation.cpp @@ -586,8 +586,13 @@ NonnullRefPtr interpolate_value(DOM::Element& element, Calc layout_node = *node; return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta), ColorSyntax::Modern); } - case CSSStyleValue::Type::Integer: - return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta)); + case CSSStyleValue::Type::Integer: { + // https://drafts.csswg.org/css-values/#combine-integers + // Interpolation of is defined as Vresult = round((1 - p) × VA + p × VB); + // that is, interpolation happens in the real number space as for s, and the result is converted to an by rounding to the nearest integer. + auto interpolated_value = interpolate_raw(from.as_integer().value(), to.as_integer().value(), delta); + return IntegerStyleValue::create(round_to(interpolated_value)); + } case CSSStyleValue::Type::Length: { // FIXME: Absolutize values auto const& from_length = from.as_length().length(); diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt new file mode 100644 index 00000000000..c434b34d0ce --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt @@ -0,0 +1,256 @@ +Harness status: OK + +Found 250 tests + +170 Pass +80 Fail +Fail CSS Transitions: property from neutral to [5] at (-0.3) should be [-4] +Fail CSS Transitions: property from neutral to [5] at (0) should be [-2] +Fail CSS Transitions: property from neutral to [5] at (0.3) should be [0] +Fail CSS Transitions: property from neutral to [5] at (0.6) should be [2] +Pass CSS Transitions: property from neutral to [5] at (1) should be [5] +Fail CSS Transitions: property from neutral to [5] at (1.5) should be [9] +Fail CSS Transitions with transition: all: property from neutral to [5] at (-0.3) should be [-4] +Fail CSS Transitions with transition: all: property from neutral to [5] at (0) should be [-2] +Fail CSS Transitions with transition: all: property from neutral to [5] at (0.3) should be [0] +Fail CSS Transitions with transition: all: property from neutral to [5] at (0.6) should be [2] +Pass CSS Transitions with transition: all: property from neutral to [5] at (1) should be [5] +Fail CSS Transitions with transition: all: property from neutral to [5] at (1.5) should be [9] +Fail CSS Animations: property from neutral to [5] at (-0.3) should be [-4] +Fail CSS Animations: property from neutral to [5] at (0) should be [-2] +Fail CSS Animations: property from neutral to [5] at (0.3) should be [0] +Fail CSS Animations: property from neutral to [5] at (0.6) should be [2] +Pass CSS Animations: property from neutral to [5] at (1) should be [5] +Fail CSS Animations: property from neutral to [5] at (1.5) should be [9] +Fail Web Animations: property from neutral to [5] at (-0.3) should be [-4] +Fail Web Animations: property from neutral to [5] at (0) should be [-2] +Fail Web Animations: property from neutral to [5] at (0.3) should be [0] +Fail Web Animations: property from neutral to [5] at (0.6) should be [2] +Pass Web Animations: property from neutral to [5] at (1) should be [5] +Fail Web Animations: property from neutral to [5] at (1.5) should be [9] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (-0.3) should be [initial] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0) should be [initial] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0.3) should be [initial] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (1.5) should be [5] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (-0.3) should be [initial] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0) should be [initial] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0.3) should be [initial] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (1.5) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (-0.3) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0.3) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (1.5) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (-0.3) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0.3) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (1.5) should be [5] +Pass CSS Animations: property from [initial] to [5] at (-0.3) should be [initial] +Pass CSS Animations: property from [initial] to [5] at (0) should be [initial] +Pass CSS Animations: property from [initial] to [5] at (0.3) should be [initial] +Pass CSS Animations: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Animations: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Animations: property from [initial] to [5] at (1) should be [5] +Pass CSS Animations: property from [initial] to [5] at (1.5) should be [5] +Pass Web Animations: property from [initial] to [5] at (-0.3) should be [initial] +Pass Web Animations: property from [initial] to [5] at (0) should be [initial] +Pass Web Animations: property from [initial] to [5] at (0.3) should be [initial] +Pass Web Animations: property from [initial] to [5] at (0.5) should be [5] +Pass Web Animations: property from [initial] to [5] at (0.6) should be [5] +Pass Web Animations: property from [initial] to [5] at (1) should be [5] +Pass Web Animations: property from [initial] to [5] at (1.5) should be [5] +Fail CSS Transitions: property from [inherit] to [5] at (-0.3) should be [18] +Fail CSS Transitions: property from [inherit] to [5] at (0) should be [15] +Fail CSS Transitions: property from [inherit] to [5] at (0.3) should be [12] +Fail CSS Transitions: property from [inherit] to [5] at (0.6) should be [9] +Pass CSS Transitions: property from [inherit] to [5] at (1) should be [5] +Fail CSS Transitions: property from [inherit] to [5] at (1.5) should be [0] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (-0.3) should be [18] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (0) should be [15] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (0.3) should be [12] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (0.6) should be [9] +Pass CSS Transitions with transition: all: property from [inherit] to [5] at (1) should be [5] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (1.5) should be [0] +Pass CSS Animations: property from [inherit] to [5] at (-0.3) should be [18] +Pass CSS Animations: property from [inherit] to [5] at (0) should be [15] +Pass CSS Animations: property from [inherit] to [5] at (0.3) should be [12] +Pass CSS Animations: property from [inherit] to [5] at (0.6) should be [9] +Pass CSS Animations: property from [inherit] to [5] at (1) should be [5] +Pass CSS Animations: property from [inherit] to [5] at (1.5) should be [0] +Pass Web Animations: property from [inherit] to [5] at (-0.3) should be [18] +Pass Web Animations: property from [inherit] to [5] at (0) should be [15] +Pass Web Animations: property from [inherit] to [5] at (0.3) should be [12] +Pass Web Animations: property from [inherit] to [5] at (0.6) should be [9] +Pass Web Animations: property from [inherit] to [5] at (1) should be [5] +Pass Web Animations: property from [inherit] to [5] at (1.5) should be [0] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (-0.3) should be [unset] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0) should be [unset] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0.3) should be [unset] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (1.5) should be [5] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (-0.3) should be [unset] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0) should be [unset] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0.3) should be [unset] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (1.5) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (-0.3) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0.3) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (1.5) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (-0.3) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0.3) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (1.5) should be [5] +Pass CSS Animations: property from [unset] to [5] at (-0.3) should be [unset] +Pass CSS Animations: property from [unset] to [5] at (0) should be [unset] +Pass CSS Animations: property from [unset] to [5] at (0.3) should be [unset] +Pass CSS Animations: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Animations: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Animations: property from [unset] to [5] at (1) should be [5] +Pass CSS Animations: property from [unset] to [5] at (1.5) should be [5] +Pass Web Animations: property from [unset] to [5] at (-0.3) should be [unset] +Pass Web Animations: property from [unset] to [5] at (0) should be [unset] +Pass Web Animations: property from [unset] to [5] at (0.3) should be [unset] +Pass Web Animations: property from [unset] to [5] at (0.5) should be [5] +Pass Web Animations: property from [unset] to [5] at (0.6) should be [5] +Pass Web Animations: property from [unset] to [5] at (1) should be [5] +Pass Web Animations: property from [unset] to [5] at (1.5) should be [5] +Fail CSS Transitions: property from [-5] to [5] at (-0.3) should be [-8] +Fail CSS Transitions: property from [-5] to [5] at (0) should be [-5] +Fail CSS Transitions: property from [-5] to [5] at (0.3) should be [-2] +Fail CSS Transitions: property from [-5] to [5] at (0.6) should be [1] +Pass CSS Transitions: property from [-5] to [5] at (1) should be [5] +Fail CSS Transitions: property from [-5] to [5] at (1.5) should be [10] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (-0.3) should be [-8] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (0) should be [-5] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (0.3) should be [-2] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (0.6) should be [1] +Pass CSS Transitions with transition: all: property from [-5] to [5] at (1) should be [5] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (1.5) should be [10] +Pass CSS Animations: property from [-5] to [5] at (-0.3) should be [-8] +Pass CSS Animations: property from [-5] to [5] at (0) should be [-5] +Pass CSS Animations: property from [-5] to [5] at (0.3) should be [-2] +Pass CSS Animations: property from [-5] to [5] at (0.6) should be [1] +Pass CSS Animations: property from [-5] to [5] at (1) should be [5] +Pass CSS Animations: property from [-5] to [5] at (1.5) should be [10] +Pass Web Animations: property from [-5] to [5] at (-0.3) should be [-8] +Pass Web Animations: property from [-5] to [5] at (0) should be [-5] +Pass Web Animations: property from [-5] to [5] at (0.3) should be [-2] +Pass Web Animations: property from [-5] to [5] at (0.6) should be [1] +Pass Web Animations: property from [-5] to [5] at (1) should be [5] +Pass Web Animations: property from [-5] to [5] at (1.5) should be [10] +Fail CSS Transitions: property from [2] to [4] at (-0.3) should be [1] +Fail CSS Transitions: property from [2] to [4] at (0) should be [2] +Fail CSS Transitions: property from [2] to [4] at (0.3) should be [3] +Fail CSS Transitions: property from [2] to [4] at (0.6) should be [3] +Pass CSS Transitions: property from [2] to [4] at (1) should be [4] +Fail CSS Transitions: property from [2] to [4] at (1.5) should be [5] +Fail CSS Transitions with transition: all: property from [2] to [4] at (-0.3) should be [1] +Fail CSS Transitions with transition: all: property from [2] to [4] at (0) should be [2] +Fail CSS Transitions with transition: all: property from [2] to [4] at (0.3) should be [3] +Fail CSS Transitions with transition: all: property from [2] to [4] at (0.6) should be [3] +Pass CSS Transitions with transition: all: property from [2] to [4] at (1) should be [4] +Fail CSS Transitions with transition: all: property from [2] to [4] at (1.5) should be [5] +Pass CSS Animations: property from [2] to [4] at (-0.3) should be [1] +Pass CSS Animations: property from [2] to [4] at (0) should be [2] +Pass CSS Animations: property from [2] to [4] at (0.3) should be [3] +Pass CSS Animations: property from [2] to [4] at (0.6) should be [3] +Pass CSS Animations: property from [2] to [4] at (1) should be [4] +Pass CSS Animations: property from [2] to [4] at (1.5) should be [5] +Pass Web Animations: property from [2] to [4] at (-0.3) should be [1] +Pass Web Animations: property from [2] to [4] at (0) should be [2] +Pass Web Animations: property from [2] to [4] at (0.3) should be [3] +Pass Web Animations: property from [2] to [4] at (0.6) should be [3] +Pass Web Animations: property from [2] to [4] at (1) should be [4] +Pass Web Animations: property from [2] to [4] at (1.5) should be [5] +Fail CSS Transitions: property from [-2] to [-4] at (-0.3) should be [-1] +Fail CSS Transitions: property from [-2] to [-4] at (0) should be [-2] +Fail CSS Transitions: property from [-2] to [-4] at (0.1) should be [-2] +Fail CSS Transitions: property from [-2] to [-4] at (0.3) should be [-3] +Fail CSS Transitions: property from [-2] to [-4] at (0.6) should be [-3] +Pass CSS Transitions: property from [-2] to [-4] at (1) should be [-4] +Fail CSS Transitions: property from [-2] to [-4] at (1.5) should be [-5] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (-0.3) should be [-1] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0) should be [-2] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0.1) should be [-2] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0.3) should be [-3] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0.6) should be [-3] +Pass CSS Transitions with transition: all: property from [-2] to [-4] at (1) should be [-4] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (1.5) should be [-5] +Pass CSS Animations: property from [-2] to [-4] at (-0.3) should be [-1] +Pass CSS Animations: property from [-2] to [-4] at (0) should be [-2] +Pass CSS Animations: property from [-2] to [-4] at (0.1) should be [-2] +Pass CSS Animations: property from [-2] to [-4] at (0.3) should be [-3] +Pass CSS Animations: property from [-2] to [-4] at (0.6) should be [-3] +Pass CSS Animations: property from [-2] to [-4] at (1) should be [-4] +Pass CSS Animations: property from [-2] to [-4] at (1.5) should be [-5] +Pass Web Animations: property from [-2] to [-4] at (-0.3) should be [-1] +Pass Web Animations: property from [-2] to [-4] at (0) should be [-2] +Pass Web Animations: property from [-2] to [-4] at (0.1) should be [-2] +Pass Web Animations: property from [-2] to [-4] at (0.3) should be [-3] +Pass Web Animations: property from [-2] to [-4] at (0.6) should be [-3] +Pass Web Animations: property from [-2] to [-4] at (1) should be [-4] +Pass Web Animations: property from [-2] to [-4] at (1.5) should be [-5] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (-0.3) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0.3) should be [auto] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (1.5) should be [10] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (-0.3) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0.3) should be [auto] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (1.5) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (-0.3) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0.3) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (1.5) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (-0.3) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0.3) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (1.5) should be [10] +Pass CSS Animations: property from [auto] to [10] at (-0.3) should be [auto] +Pass CSS Animations: property from [auto] to [10] at (0) should be [auto] +Pass CSS Animations: property from [auto] to [10] at (0.3) should be [auto] +Pass CSS Animations: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Animations: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Animations: property from [auto] to [10] at (1) should be [10] +Pass CSS Animations: property from [auto] to [10] at (1.5) should be [10] +Pass Web Animations: property from [auto] to [10] at (-0.3) should be [auto] +Pass Web Animations: property from [auto] to [10] at (0) should be [auto] +Pass Web Animations: property from [auto] to [10] at (0.3) should be [auto] +Pass Web Animations: property from [auto] to [10] at (0.5) should be [10] +Pass Web Animations: property from [auto] to [10] at (0.6) should be [10] +Pass Web Animations: property from [auto] to [10] at (1) should be [10] +Pass Web Animations: property from [auto] to [10] at (1.5) should be [10] \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html new file mode 100644 index 00000000000..c7614536950 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html @@ -0,0 +1,130 @@ + + +z-index interpolation + + + + + + + + + + + + From f07a3fe6dab850bc86089fd46ae0f0b70f016924 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Fri, 11 Apr 2025 09:59:25 +0100 Subject: [PATCH 61/83] LibWeb: Use discrete interpolation for degenerate ratios Degenerate ratios cannot be interpolated. --- Libraries/LibWeb/CSS/Interpolation.cpp | 5 + .../animation/aspect-ratio-interpolation.txt | 252 ++++++++++++++++++ .../animation/aspect-ratio-interpolation.html | 130 +++++++++ 3 files changed, 387 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html diff --git a/Libraries/LibWeb/CSS/Interpolation.cpp b/Libraries/LibWeb/CSS/Interpolation.cpp index 282c2795c6f..c87cb4f3853 100644 --- a/Libraries/LibWeb/CSS/Interpolation.cpp +++ b/Libraries/LibWeb/CSS/Interpolation.cpp @@ -616,6 +616,11 @@ NonnullRefPtr interpolate_value(DOM::Element& element, Calc auto from_ratio = from.as_ratio().ratio(); auto to_ratio = to.as_ratio().ratio(); + // https://drafts.csswg.org/css-values/#combine-ratio + // If either is degenerate, the values cannot be interpolated. + if (from_ratio.is_degenerate() || to_ratio.is_degenerate()) + return delta >= 0.5f ? to : from; + // The interpolation of a is defined by converting each to a number by dividing the first value // by the second (so a ratio of 3 / 2 would become 1.5), taking the logarithm of that result (so the 1.5 would // become approximately 0.176), then interpolating those values. The result during the interpolation is diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt new file mode 100644 index 00000000000..e597bba2f51 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt @@ -0,0 +1,252 @@ +Harness status: OK + +Found 246 tests + +184 Pass +62 Fail +Fail CSS Transitions: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions: property from [0.5] to [2] at (0) should be [0.5 / 1] +Fail CSS Transitions: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [0.5] to [2] at (1) should be [2 / 1] +Fail CSS Transitions: property from [0.5] to [2] at (1.5) should be [4 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (0) should be [0.5 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [0.5] to [2] at (1) should be [2 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (1.5) should be [4 / 1] +Pass CSS Animations: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Pass CSS Animations: property from [0.5] to [2] at (0) should be [0.5 / 1] +Pass CSS Animations: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [0.5] to [2] at (1) should be [2 / 1] +Pass CSS Animations: property from [0.5] to [2] at (1.5) should be [4 / 1] +Pass Web Animations: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Pass Web Animations: property from [0.5] to [2] at (0) should be [0.5 / 1] +Pass Web Animations: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass Web Animations: property from [0.5] to [2] at (1) should be [2 / 1] +Pass Web Animations: property from [0.5] to [2] at (1.5) should be [4 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Pass CSS Animations: property from [auto] to [2 / 1] at (0) should be [auto] +Pass CSS Animations: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass CSS Animations: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Pass Web Animations: property from [auto] to [2 / 1] at (0) should be [auto] +Pass Web Animations: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass Web Animations: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (-0.3) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0.3) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (-0.3) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0.3) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass Compositing: property underlying [2 / 1] from replace [0.5 / 1] to add [1 / 1] at (0) should be [0.5 / 1] +Fail Compositing: property underlying [2 / 1] from replace [0.5 / 1] to add [1 / 1] at (0.5) should be [1 / 1] +Fail Compositing: property underlying [2 / 1] from replace [0.5 / 1] to add [1 / 1] at (1) should be [2 / 1] \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html b/Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html new file mode 100644 index 00000000000..980767f7195 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html @@ -0,0 +1,130 @@ + + +aspect-ratio interpolation + + + + + + + + + + + + + + + From 87bffe7d22b58cf441e98b91af9b4b433fe34a1d Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Fri, 11 Apr 2025 15:04:24 +0200 Subject: [PATCH 62/83] Tests: Rearrange log order in Messaging-post-channel-over-channel.html When a message is posted to multiple ports at once, the order in which the callbacks for these messages are invoked is non-deterministic. To account for this, the test has been rewritten to accumulate logs for each port separately, and then print them grouped by port. --- .../Messaging-post-channel-over-channel.txt | 4 ++-- .../Messaging-post-channel-over-channel.html | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt b/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt index 0ebf505f534..f3b99ef8537 100644 --- a/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt +++ b/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt @@ -1,6 +1,6 @@ Port1: "Hello" Port1: {"foo":{}} -Port2: "Hello" -Port3: "Hello from the transferred port" Port1: "DONE" +Port2: "Hello" Port2: "DONE" +Port3: "Hello from the transferred port" diff --git a/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html b/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html index 7ef3b100abd..d386d0e9aba 100644 --- a/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html +++ b/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html @@ -4,8 +4,12 @@ asyncTest(done => { let channel = new MessageChannel(); + const port1Logs = []; + const port2Logs = []; + const port3Logs = []; + channel.port1.onmessage = (event) => { - println("Port1: " + JSON.stringify(event.data)); + port1Logs.push("Port1: " + JSON.stringify(event.data)); if (event.ports.length > 0) { event.ports[0].postMessage("Hello from the transferred port"); return; @@ -14,8 +18,14 @@ }; channel.port2.onmessage = (event) => { - println("Port2: " + JSON.stringify(event.data)); + port2Logs.push("Port2: " + JSON.stringify(event.data)); if (event.data === "DONE") { + for (let log of port1Logs) + println(log); + for (let log of port2Logs) + println(log); + for (let log of port3Logs) + println(log); done(); } }; @@ -23,7 +33,7 @@ let channel2 = new MessageChannel(); channel2.port2.onmessage = (event) => { - println("Port3: " + JSON.stringify(event.data)); + port3Logs.push("Port3: " + JSON.stringify(event.data)); channel.port2.postMessage("DONE"); } From cceb4321fce0ae1a7200f5a559c8d26bdeb0e944 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 16:49:02 -0400 Subject: [PATCH 63/83] LibGC: Allow visiting vectors with inline capacity This allows visiting e.g. Vector. --- Libraries/LibGC/Cell.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/LibGC/Cell.h b/Libraries/LibGC/Cell.h index 4b857db94d1..5c5260715d0 100644 --- a/Libraries/LibGC/Cell.h +++ b/Libraries/LibGC/Cell.h @@ -107,8 +107,8 @@ public: visit(value); } - template - void visit(Vector const& vector) + template + void visit(Vector const& vector) { for (auto& value : vector) visit(value); From f7c095a318602d629a858ad820925fb844256eed Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 11:46:54 -0400 Subject: [PATCH 64/83] LibWeb: Implement an AO to get a promise to wait for promises to settle --- Libraries/LibWeb/WebIDL/Promise.cpp | 32 +++++++++++++++++++++++++++++ Libraries/LibWeb/WebIDL/Promise.h | 1 + 2 files changed, 33 insertions(+) diff --git a/Libraries/LibWeb/WebIDL/Promise.cpp b/Libraries/LibWeb/WebIDL/Promise.cpp index cf1180e5d0c..c2bbb0ccbb6 100644 --- a/Libraries/LibWeb/WebIDL/Promise.cpp +++ b/Libraries/LibWeb/WebIDL/Promise.cpp @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace Web::WebIDL { @@ -290,6 +292,36 @@ void wait_for_all(JS::Realm& realm, Vector> const& promises, Fu } } +// https://webidl.spec.whatwg.org/#waiting-for-all-promise +GC::Ref get_promise_for_wait_for_all(JS::Realm& realm, Vector> const& promises) +{ + // 1. Let promise be a new promise of type Promise> in realm. + auto promise = create_promise(realm); + + // 2. Let successSteps be the following steps, given results: + auto success_steps = [&realm, promise](Vector const& results) { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. Resolve promise with results. + auto sequence = JS::Array::create_from(realm, results); + resolve_promise(realm, promise, sequence); + }; + + // 3. Let failureSteps be the following steps, given reason: + auto failure_steps = [&realm, promise](JS::Value reason) { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. Reject promise with reason. + reject_promise(realm, promise, reason); + }; + + // 4. Wait for all with promises, given successSteps and failureSteps. + wait_for_all(realm, promises, move(success_steps), move(failure_steps)); + + // 5. Return promise. + return promise; +} + GC::Ref create_rejected_promise_from_exception(JS::Realm& realm, Exception exception) { auto throw_completion = Bindings::exception_to_throw_completion(realm.vm(), move(exception)); diff --git a/Libraries/LibWeb/WebIDL/Promise.h b/Libraries/LibWeb/WebIDL/Promise.h index 555c6f31c3b..82e2fa30b5d 100644 --- a/Libraries/LibWeb/WebIDL/Promise.h +++ b/Libraries/LibWeb/WebIDL/Promise.h @@ -30,6 +30,7 @@ GC::Ref upon_fulfillment(Promise const&, GC::Ref); GC::Ref upon_rejection(Promise const&, GC::Ref); void mark_promise_as_handled(Promise const&); void wait_for_all(JS::Realm&, Vector> const& promises, Function const&)> success_steps, Function failure_steps); +GC::Ref get_promise_for_wait_for_all(JS::Realm&, Vector> const& promises); // Non-spec, convenience method. GC::Ref create_rejected_promise_from_exception(JS::Realm&, Exception); From 1d6e1637cc6e588c6d5606981e4958a9a883a2a8 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 11:56:29 -0400 Subject: [PATCH 65/83] LibWeb: Implement an AO to close writable streams with error propagation --- .../LibWeb/Streams/AbstractOperations.cpp | 29 +++++++++++++++++++ Libraries/LibWeb/Streams/AbstractOperations.h | 1 + 2 files changed, 30 insertions(+) diff --git a/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Libraries/LibWeb/Streams/AbstractOperations.cpp index 1960bd6a409..9e14361459a 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -4092,6 +4092,35 @@ GC::Ref writable_stream_default_writer_close(WritableStreamDefa return writable_stream_close(*stream); } +// https://streams.spec.whatwg.org/#writable-stream-default-writer-close-with-error-propagation +GC::Ref writable_stream_default_writer_close_with_error_propagation(WritableStreamDefaultWriter& writer) +{ + auto& realm = writer.realm(); + + // 1. Let stream be writer.[[stream]]. + auto stream = writer.stream(); + + // 2. Assert: stream is not undefined. + VERIFY(stream); + + // 3. Let state be stream.[[state]]. + auto state = stream->state(); + + // 4. If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state is "closed", return a promise resolved with undefined. + if (writable_stream_close_queued_or_in_flight(*stream) || state == WritableStream::State::Closed) + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + + // 5. If state is "errored", return a promise rejected with stream.[[storedError]]. + if (state == WritableStream::State::Errored) + return WebIDL::create_rejected_promise(realm, stream->stored_error()); + + // 6. Assert: state is "writable" or "erroring". + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + + // 7. Return ! WritableStreamDefaultWriterClose(writer). + return writable_stream_default_writer_close(writer); +} + // https://streams.spec.whatwg.org/#writable-stream-default-writer-ensure-closed-promise-rejected void writable_stream_default_writer_ensure_closed_promise_rejected(WritableStreamDefaultWriter& writer, JS::Value error) { diff --git a/Libraries/LibWeb/Streams/AbstractOperations.h b/Libraries/LibWeb/Streams/AbstractOperations.h index 91e5f711646..94817cb223d 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.h +++ b/Libraries/LibWeb/Streams/AbstractOperations.h @@ -135,6 +135,7 @@ void writable_stream_update_backpressure(WritableStream&, bool backpressure); GC::Ref writable_stream_default_writer_abort(WritableStreamDefaultWriter&, JS::Value reason); GC::Ref writable_stream_default_writer_close(WritableStreamDefaultWriter&); +GC::Ref writable_stream_default_writer_close_with_error_propagation(WritableStreamDefaultWriter&); void writable_stream_default_writer_ensure_closed_promise_rejected(WritableStreamDefaultWriter&, JS::Value error); void writable_stream_default_writer_ensure_ready_promise_rejected(WritableStreamDefaultWriter&, JS::Value error); Optional writable_stream_default_writer_get_desired_size(WritableStreamDefaultWriter const&); From ab43c3be233d969a3e6dfe427e4fa094edc83df6 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 10 Apr 2025 16:00:39 -0400 Subject: [PATCH 66/83] LibWeb: Store WritableStream's strategy high water mark as a double It is received from user JS as a double and is only used as a double in all subsequent calculations. This bug would cause UBSAN errors in an upcoming imported WPT test, which passes Infinity as the HWM. Note there is an equivalent HWM for ReadableStream, which already stores the value as a double. --- Libraries/LibWeb/Streams/WritableStreamDefaultController.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/LibWeb/Streams/WritableStreamDefaultController.h b/Libraries/LibWeb/Streams/WritableStreamDefaultController.h index 5a0fa8fafb4..4ffc70f3663 100644 --- a/Libraries/LibWeb/Streams/WritableStreamDefaultController.h +++ b/Libraries/LibWeb/Streams/WritableStreamDefaultController.h @@ -38,8 +38,8 @@ public: bool started() const { return m_started; } void set_started(bool value) { m_started = value; } - size_t strategy_hwm() const { return m_strategy_hwm; } - void set_strategy_hwm(size_t value) { m_strategy_hwm = value; } + double strategy_hwm() const { return m_strategy_hwm; } + void set_strategy_hwm(double value) { m_strategy_hwm = value; } GC::Ptr strategy_size_algorithm() { return m_strategy_size_algorithm; } void set_strategy_size_algorithm(GC::Ptr value) { m_strategy_size_algorithm = value; } @@ -86,7 +86,7 @@ private: // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-strategyhwm // A number supplied by the creator of the stream as part of the stream’s queuing strategy, indicating the point at which the stream will apply backpressure to its underlying sink - size_t m_strategy_hwm { 0 }; + double m_strategy_hwm { 0 }; // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-strategysizealgorithm // An algorithm to calculate the size of enqueued chunks, as part of the stream’s queuing strategy From 3033929bb63cb1ffb402295df2bc29260b00be3f Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 08:32:19 -0400 Subject: [PATCH 67/83] LibWeb: Pass abort signal as its concrete type to ReadableStreamPipeTo There's no real need to wrap it in a JS::Value just to unrwap it again. --- Libraries/LibWeb/Streams/AbstractOperations.cpp | 6 ++---- Libraries/LibWeb/Streams/AbstractOperations.h | 2 +- Libraries/LibWeb/Streams/ReadableStream.cpp | 6 +++--- Libraries/LibWeb/Streams/ReadableStream.h | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Libraries/LibWeb/Streams/AbstractOperations.cpp index 9e14361459a..448c0cf7811 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -290,7 +290,7 @@ bool readable_stream_has_default_reader(ReadableStream const& stream) } // https://streams.spec.whatwg.org/#readable-stream-pipe-to -GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool, bool, bool, JS::Value signal) +GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool, bool, bool, GC::Ptr signal) { auto& realm = source.realm(); @@ -299,10 +299,8 @@ GC::Ref readable_stream_pipe_to(ReadableStream& source, Writabl // 3. Assert: preventClose, preventAbort, and preventCancel are all booleans. // 4. If signal was not given, let signal be undefined. - // NOTE: Done by default argument - // 5. Assert: either signal is undefined, or signal implements AbortSignal. - VERIFY(signal.is_undefined() || (signal.is_object() && is(signal.as_object()))); + (void)signal; // 6. Assert: ! IsReadableStreamLocked(source) is false. VERIFY(!is_readable_stream_locked(source)); diff --git a/Libraries/LibWeb/Streams/AbstractOperations.h b/Libraries/LibWeb/Streams/AbstractOperations.h index 94817cb223d..7683c8d5e56 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.h +++ b/Libraries/LibWeb/Streams/AbstractOperations.h @@ -40,7 +40,7 @@ size_t readable_stream_get_num_read_requests(ReadableStream const&); bool readable_stream_has_byob_reader(ReadableStream const&); bool readable_stream_has_default_reader(ReadableStream const&); -GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool prevent_close, bool prevent_abort, bool prevent_cancel, JS::Value signal = JS::js_undefined()); +GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool prevent_close, bool prevent_abort, bool prevent_cancel, GC::Ptr signal = {}); WebIDL::ExceptionOr readable_stream_tee(JS::Realm&, ReadableStream&, bool clone_for_branch2); WebIDL::ExceptionOr readable_stream_default_tee(JS::Realm& realm, ReadableStream& stream, bool clone_for_branch2); diff --git a/Libraries/LibWeb/Streams/ReadableStream.cpp b/Libraries/LibWeb/Streams/ReadableStream.cpp index 91e1373e084..c67fc50e1e0 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.cpp +++ b/Libraries/LibWeb/Streams/ReadableStream.cpp @@ -135,7 +135,7 @@ WebIDL::ExceptionOr> ReadableStream::pipe_through(Readab return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Failed to execute 'pipeThrough' on 'ReadableStream': parameter 1's 'writable' is locked"sv }; // 3. Let signal be options["signal"] if it exists, or undefined otherwise. - auto signal = options.signal ? JS::Value(options.signal) : JS::js_undefined(); + auto signal = options.signal; // 4. Let promise be ! ReadableStreamPipeTo(this, transform["writable"], options["preventClose"], options["preventAbort"], options["preventCancel"], signal). auto promise = readable_stream_pipe_to(*this, *transform.writable, options.prevent_close, options.prevent_abort, options.prevent_cancel, signal); @@ -164,7 +164,7 @@ GC::Ref ReadableStream::pipe_to(WritableStream& destination, St } // 3. Let signal be options["signal"] if it exists, or undefined otherwise. - auto signal = options.signal ? JS::Value(options.signal) : JS::js_undefined(); + auto signal = options.signal; // 4. Return ! ReadableStreamPipeTo(this, destination, options["preventClose"], options["preventAbort"], options["preventCancel"], signal). return readable_stream_pipe_to(*this, destination, options.prevent_close, options.prevent_abort, options.prevent_cancel, signal); @@ -427,7 +427,7 @@ void ReadableStream::set_up_with_byte_reading_support(GC::Ptr pul } // https://streams.spec.whatwg.org/#readablestream-pipe-through -GC::Ref ReadableStream::piped_through(GC::Ref transform, bool prevent_close, bool prevent_abort, bool prevent_cancel, JS::Value signal) +GC::Ref ReadableStream::piped_through(GC::Ref transform, bool prevent_close, bool prevent_abort, bool prevent_cancel, GC::Ptr signal) { // 1. Assert: ! IsReadableStreamLocked(readable) is false. VERIFY(!is_readable_stream_locked(*this)); diff --git a/Libraries/LibWeb/Streams/ReadableStream.h b/Libraries/LibWeb/Streams/ReadableStream.h index af7e9f4fd8c..a82c7b161e6 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.h +++ b/Libraries/LibWeb/Streams/ReadableStream.h @@ -109,7 +109,7 @@ public: WebIDL::ExceptionOr pull_from_bytes(ByteBuffer); WebIDL::ExceptionOr enqueue(JS::Value chunk); void set_up_with_byte_reading_support(GC::Ptr = {}, GC::Ptr = {}, double high_water_mark = 0); - GC::Ref piped_through(GC::Ref, bool prevent_close = false, bool prevent_abort = false, bool prevent_cancel = false, JS::Value signal = JS::js_undefined()); + GC::Ref piped_through(GC::Ref, bool prevent_close = false, bool prevent_abort = false, bool prevent_cancel = false, GC::Ptr signal = {}); GC::Ptr current_byob_request_view(); From e9a7694cdb864c00cafe5c673a49b68f1f0d5dcb Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 17:50:24 -0400 Subject: [PATCH 68/83] LibWeb: Prefer react_to_promise over upon_fulfillment + upon_rejection While debugging a spec-compliant implementation of ReadableStreamPipeTo, I spent a lot of time inspecting promise internals. This is much less noisy if we halve the number of temporary promises. --- Libraries/LibWeb/HTML/Scripting/Fetching.cpp | 55 +-- .../LibWeb/Streams/AbstractOperations.cpp | 316 +++++++++--------- 2 files changed, 190 insertions(+), 181 deletions(-) diff --git a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp index e8f93ecac49..5c01d278fa7 100644 --- a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp +++ b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp @@ -836,37 +836,38 @@ void fetch_descendants_of_and_link_a_module_script(JS::Realm& realm, // 5. Let loadingPromise be record.LoadRequestedModules(state). auto& loading_promise = record->load_requested_modules(state); - // 6. Upon fulfillment of loadingPromise, run the following steps: - WebIDL::upon_fulfillment(loading_promise, GC::create_function(realm.heap(), [&realm, record, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { - // 1. Perform record.Link(). - auto linking_result = record->link(realm.vm()); + WebIDL::react_to_promise(loading_promise, + // 6. Upon fulfillment of loadingPromise, run the following steps: + GC::create_function(realm.heap(), [&realm, record, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { + // 1. Perform record.Link(). + auto linking_result = record->link(realm.vm()); - // If this throws an exception, set result's error to rethrow to that exception. - if (linking_result.is_throw_completion()) - module_script.set_error_to_rethrow(linking_result.release_error().value()); - - // 2. Run onComplete given moduleScript. - on_complete->function()(module_script); - - return JS::js_undefined(); - })); - - // 7. Upon rejection of loadingPromise, run the following steps: - WebIDL::upon_rejection(loading_promise, GC::create_function(realm.heap(), [state, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { - // 1. If state.[[ParseError]] is not null, set moduleScript's error to rethrow to state.[[ParseError]] and run - // onComplete given moduleScript. - if (!state->parse_error.is_null()) { - module_script.set_error_to_rethrow(state->parse_error); + // If this throws an exception, set result's error to rethrow to that exception. + if (linking_result.is_throw_completion()) + module_script.set_error_to_rethrow(linking_result.release_error().value()); + // 2. Run onComplete given moduleScript. on_complete->function()(module_script); - } - // 2. Otherwise, run onComplete given null. - else { - on_complete->function()(nullptr); - } - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), + + // 7. Upon rejection of loadingPromise, run the following steps: + GC::create_function(realm.heap(), [state, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { + // 1. If state.[[ParseError]] is not null, set moduleScript's error to rethrow to state.[[ParseError]] and run + // onComplete given moduleScript. + if (!state->parse_error.is_null()) { + module_script.set_error_to_rethrow(state->parse_error); + + on_complete->function()(module_script); + } + // 2. Otherwise, run onComplete given null. + else { + on_complete->function()(nullptr); + } + + return JS::js_undefined(); + })); clean_up_after_running_callback(realm); diff --git a/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Libraries/LibWeb/Streams/AbstractOperations.cpp index 448c0cf7811..a5af1d1d0cf 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -2199,30 +2199,31 @@ void readable_stream_default_controller_can_pull_if_needed(ReadableStreamDefault // 6. Let pullPromise be the result of performing controller.[[pullAlgorithm]]. auto pull_promise = controller.pull_algorithm()->function()(); - // 7. Upon fulfillment of pullPromise, - WebIDL::upon_fulfillment(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[pulling]] to false. - controller.set_pulling(false); + WebIDL::react_to_promise(pull_promise, + // 7. Upon fulfillment of pullPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[pulling]] to false. + controller.set_pulling(false); - // 2. If controller.[[pullAgain]] is true, - if (controller.pull_again()) { - // 1. Set controller.[[pullAgain]] to false. - controller.set_pull_again(false); + // 2. If controller.[[pullAgain]] is true, + if (controller.pull_again()) { + // 1. Set controller.[[pullAgain]] to false. + controller.set_pull_again(false); - // 2. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). - readable_stream_default_controller_can_pull_if_needed(controller); - } + // 2. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + readable_stream_default_controller_can_pull_if_needed(controller); + } - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 8. Upon rejection of pullPromise with reason e, - WebIDL::upon_rejection(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value e) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableStreamDefaultControllerError(controller, e). - readable_stream_default_controller_error(controller, e); + // 8. Upon rejection of pullPromise with reason e, + GC::create_function(controller.heap(), [&controller](JS::Value e) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableStreamDefaultControllerError(controller, e). + readable_stream_default_controller_error(controller, e); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#readable-stream-default-controller-should-call-pull @@ -2626,30 +2627,31 @@ WebIDL::ExceptionOr set_up_readable_stream_default_controller(ReadableStre // 10. Let startPromise be a promise resolved with startResult. auto start_promise = WebIDL::create_resolved_promise(realm, start_result); - // 11. Upon fulfillment of startPromise, - WebIDL::upon_fulfillment(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[started]] to true. - controller.set_started(true); + WebIDL::react_to_promise(start_promise, + // 11. Upon fulfillment of startPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[started]] to true. + controller.set_started(true); - // 2. Assert: controller.[[pulling]] is false. - VERIFY(!controller.pulling()); + // 2. Assert: controller.[[pulling]] is false. + VERIFY(!controller.pulling()); - // 3. Assert: controller.[[pullAgain]] is false. - VERIFY(!controller.pull_again()); + // 3. Assert: controller.[[pullAgain]] is false. + VERIFY(!controller.pull_again()); - // 4. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). - readable_stream_default_controller_can_pull_if_needed(controller); + // 4. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + readable_stream_default_controller_can_pull_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 12. Upon rejection of startPromise with reason r, - WebIDL::upon_rejection(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableStreamDefaultControllerError(controller, r). - readable_stream_default_controller_error(controller, r); + // 12. Upon rejection of startPromise with reason r, + GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableStreamDefaultControllerError(controller, r). + readable_stream_default_controller_error(controller, r); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); return {}; } @@ -2735,30 +2737,31 @@ void readable_byte_stream_controller_call_pull_if_needed(ReadableByteStreamContr // 6. Let pullPromise be the result of performing controller.[[pullAlgorithm]]. auto pull_promise = controller.pull_algorithm()->function()(); - // 7. Upon fulfillment of pullPromise, - WebIDL::upon_fulfillment(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[pulling]] to false. - controller.set_pulling(false); + WebIDL::react_to_promise(pull_promise, + // 7. Upon fulfillment of pullPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[pulling]] to false. + controller.set_pulling(false); - // 2. If controller.[[pullAgain]] is true, - if (controller.pull_again()) { - // 1. Set controller.[[pullAgain]] to false. - controller.set_pull_again(false); + // 2. If controller.[[pullAgain]] is true, + if (controller.pull_again()) { + // 1. Set controller.[[pullAgain]] to false. + controller.set_pull_again(false); - // 2. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - readable_byte_stream_controller_call_pull_if_needed(controller); - } + // 2. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + readable_byte_stream_controller_call_pull_if_needed(controller); + } - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 8. Upon rejection of pullPromise with reason e, - WebIDL::upon_rejection(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value error) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableByteStreamControllerError(controller, e). - readable_byte_stream_controller_error(controller, error); + // 8. Upon rejection of pullPromise with reason e, + GC::create_function(controller.heap(), [&controller](JS::Value error) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableByteStreamControllerError(controller, e). + readable_byte_stream_controller_error(controller, error); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#readable-byte-stream-controller-clear-algorithms @@ -3242,30 +3245,31 @@ WebIDL::ExceptionOr set_up_readable_byte_stream_controller(ReadableStream& // 15. Let startPromise be a promise resolved with startResult. auto start_promise = WebIDL::create_resolved_promise(realm, start_result); - // 16. Upon fulfillment of startPromise, - WebIDL::upon_fulfillment(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[started]] to true. - controller.set_started(true); + WebIDL::react_to_promise(start_promise, + // 16. Upon fulfillment of startPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[started]] to true. + controller.set_started(true); - // 2. Assert: controller.[[pulling]] is false. - VERIFY(!controller.pulling()); + // 2. Assert: controller.[[pulling]] is false. + VERIFY(!controller.pulling()); - // 3. Assert: controller.[[pullAgain]] is false. - VERIFY(!controller.pull_again()); + // 3. Assert: controller.[[pullAgain]] is false. + VERIFY(!controller.pull_again()); - // 4. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - readable_byte_stream_controller_call_pull_if_needed(controller); + // 4. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + readable_byte_stream_controller_call_pull_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 17. Upon rejection of startPromise with reason r, - WebIDL::upon_rejection(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableByteStreamControllerError(controller, r). - readable_byte_stream_controller_error(controller, r); + // 17. Upon rejection of startPromise with reason r, + GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableByteStreamControllerError(controller, r). + readable_byte_stream_controller_error(controller, r); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); return {}; } @@ -3782,27 +3786,28 @@ void writable_stream_finish_erroring(WritableStream& stream) // 12. Let promise be ! stream.[[controller]].[[AbortSteps]](abortRequest’s reason). auto promise = stream.controller()->abort_steps(abort_request.reason); - // 13. Upon fulfillment of promise, - WebIDL::upon_fulfillment(*promise, GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value) -> WebIDL::ExceptionOr { - // 1. Resolve abortRequest’s promise with undefined. - WebIDL::resolve_promise(realm, abort_promise, JS::js_undefined()); + WebIDL::react_to_promise(promise, + // 13. Upon fulfillment of promise, + GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value) -> WebIDL::ExceptionOr { + // 1. Resolve abortRequest’s promise with undefined. + WebIDL::resolve_promise(realm, abort_promise, JS::js_undefined()); - // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - writable_stream_reject_close_and_closed_promise_if_needed(stream); + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 14. Upon rejection of promise with reason reason, - WebIDL::upon_rejection(*promise, GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. Reject abortRequest’s promise with reason. - WebIDL::reject_promise(realm, abort_promise, reason); + // 14. Upon rejection of promise with reason reason, + GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Reject abortRequest’s promise with reason. + WebIDL::reject_promise(realm, abort_promise, reason); - // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - writable_stream_reject_close_and_closed_promise_if_needed(stream); + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#writable-stream-finish-in-flight-close @@ -4313,35 +4318,36 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller(WritableStre // 16. Let startPromise be a promise resolved with startResult. auto start_promise = WebIDL::create_resolved_promise(realm, start_result); - // 17. Upon fulfillment of startPromise, - WebIDL::upon_fulfillment(*start_promise, GC::create_function(realm.heap(), [&controller, &stream](JS::Value) -> WebIDL::ExceptionOr { - // 1. Assert: stream.[[state]] is "writable" or "erroring". - auto state = stream.state(); - VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + WebIDL::react_to_promise(start_promise, + // 17. Upon fulfillment of startPromise, + GC::create_function(realm.heap(), [&controller, &stream](JS::Value) -> WebIDL::ExceptionOr { + // 1. Assert: stream.[[state]] is "writable" or "erroring". + auto state = stream.state(); + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); - // 2. Set controller.[[started]] to true. - controller.set_started(true); + // 2. Set controller.[[started]] to true. + controller.set_started(true); - // 3. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - writable_stream_default_controller_advance_queue_if_needed(controller); + // 3. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + writable_stream_default_controller_advance_queue_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 18. Upon rejection of startPromise with reason r, - WebIDL::upon_rejection(*start_promise, GC::create_function(realm.heap(), [&stream, &controller](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. Assert: stream.[[state]] is "writable" or "erroring". - auto state = stream.state(); - VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + // 18. Upon rejection of startPromise with reason r, + GC::create_function(realm.heap(), [&stream, &controller](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Assert: stream.[[state]] is "writable" or "erroring". + auto state = stream.state(); + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); - // 2. Set controller.[[started]] to true. - controller.set_started(true); + // 2. Set controller.[[started]] to true. + controller.set_started(true); - // 3. Perform ! WritableStreamDealWithRejection(stream, r). - writable_stream_deal_with_rejection(stream, reason); + // 3. Perform ! WritableStreamDealWithRejection(stream, r). + writable_stream_deal_with_rejection(stream, reason); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); return {}; } @@ -4575,21 +4581,22 @@ void writable_stream_default_controller_process_close(WritableStreamDefaultContr // 6. Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). writable_stream_default_controller_clear_algorithms(controller); - // 7. Upon fulfillment of sinkClosePromise, - WebIDL::upon_fulfillment(*sink_close_promise, GC::create_function(controller.heap(), [stream](JS::Value) -> WebIDL::ExceptionOr { - // 1. Perform ! WritableStreamFinishInFlightClose(stream). - writable_stream_finish_in_flight_close(*stream); + WebIDL::react_to_promise(sink_close_promise, + // 7. Upon fulfillment of sinkClosePromise, + GC::create_function(controller.heap(), [stream](JS::Value) -> WebIDL::ExceptionOr { + // 1. Perform ! WritableStreamFinishInFlightClose(stream). + writable_stream_finish_in_flight_close(*stream); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 8. Upon rejection of sinkClosePromise with reason reason, - WebIDL::upon_rejection(*sink_close_promise, GC::create_function(controller.heap(), [stream = stream](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. Perform ! WritableStreamFinishInFlightCloseWithError(stream, reason). - writable_stream_finish_in_flight_close_with_error(*stream, reason); + // 8. Upon rejection of sinkClosePromise with reason reason, + GC::create_function(controller.heap(), [stream = stream](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Perform ! WritableStreamFinishInFlightCloseWithError(stream, reason). + writable_stream_finish_in_flight_close_with_error(*stream, reason); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#writable-stream-default-controller-process-write @@ -4604,46 +4611,47 @@ void writable_stream_default_controller_process_write(WritableStreamDefaultContr // 3. Let sinkWritePromise be the result of performing controller.[[writeAlgorithm]], passing in chunk. auto sink_write_promise = controller.write_algorithm()->function()(chunk); - // 4. Upon fulfillment of sinkWritePromise, - WebIDL::upon_fulfillment(*sink_write_promise, GC::create_function(controller.heap(), [&controller, stream](JS::Value) -> WebIDL::ExceptionOr { - // 1. Perform ! WritableStreamFinishInFlightWrite(stream). - writable_stream_finish_in_flight_write(*stream); + WebIDL::react_to_promise(sink_write_promise, + // 4. Upon fulfillment of sinkWritePromise, + GC::create_function(controller.heap(), [&controller, stream](JS::Value) -> WebIDL::ExceptionOr { + // 1. Perform ! WritableStreamFinishInFlightWrite(stream). + writable_stream_finish_in_flight_write(*stream); - // 2. Let state be stream.[[state]]. - auto state = stream->state(); + // 2. Let state be stream.[[state]]. + auto state = stream->state(); - // 3. Assert: state is "writable" or "erroring". - VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + // 3. Assert: state is "writable" or "erroring". + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); - // 4. Perform ! DequeueValue(controller). - dequeue_value(controller); + // 4. Perform ! DequeueValue(controller). + dequeue_value(controller); - // 5. If ! WritableStreamCloseQueuedOrInFlight(stream) is false and state is "writable", - if (!writable_stream_close_queued_or_in_flight(*stream) && state == WritableStream::State::Writable) { - // 1. Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). - auto backpressure = writable_stream_default_controller_get_backpressure(controller); + // 5. If ! WritableStreamCloseQueuedOrInFlight(stream) is false and state is "writable", + if (!writable_stream_close_queued_or_in_flight(*stream) && state == WritableStream::State::Writable) { + // 1. Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). + auto backpressure = writable_stream_default_controller_get_backpressure(controller); - // 2. Perform ! WritableStreamUpdateBackpressure(stream, backpressure). - writable_stream_update_backpressure(*stream, backpressure); - } + // 2. Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + writable_stream_update_backpressure(*stream, backpressure); + } - // 6 .Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - writable_stream_default_controller_advance_queue_if_needed(controller); + // 6 .Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + writable_stream_default_controller_advance_queue_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 5. Upon rejection of sinkWritePromise with reason, - WebIDL::upon_rejection(*sink_write_promise, GC::create_function(controller.heap(), [&controller, stream](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. If stream.[[state]] is "writable", perform ! WritableStreamDefaultControllerClearAlgorithms(controller). - if (stream->state() == WritableStream::State::Writable) - writable_stream_default_controller_clear_algorithms(controller); + // 5. Upon rejection of sinkWritePromise with reason, + GC::create_function(controller.heap(), [&controller, stream](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. If stream.[[state]] is "writable", perform ! WritableStreamDefaultControllerClearAlgorithms(controller). + if (stream->state() == WritableStream::State::Writable) + writable_stream_default_controller_clear_algorithms(controller); - // 2. Perform ! WritableStreamFinishInFlightWriteWithError(stream, reason). - writable_stream_finish_in_flight_write_with_error(*stream, reason); + // 2. Perform ! WritableStreamFinishInFlightWriteWithError(stream, reason). + writable_stream_finish_in_flight_write_with_error(*stream, reason); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#writable-stream-default-controller-write From f268f24dd5cc28354d179cf9cbf70ca9db020ef6 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 18:19:34 -0400 Subject: [PATCH 69/83] LibWeb: Explicitly rethrow exceptions from writable stream `start` This is an editorial change in the Streams spec. See: https://github.com/whatwg/streams/commit/95a5adf --- Libraries/LibWeb/Streams/AbstractOperations.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Libraries/LibWeb/Streams/AbstractOperations.cpp index a5af1d1d0cf..e34b1ebfbd8 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -4378,15 +4378,19 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller_from_underly return WebIDL::create_resolved_promise(realm, JS::js_undefined()); }); - // 6. If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["start"] with argument list « controller » and callback this value underlyingSink. + // 6. If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of + // invoking underlyingSinkDict["start"] with argument list « controller », exception behavior "rethrow", and + // callback this value underlyingSink. if (underlying_sink.start) { start_algorithm = GC::create_function(realm.heap(), [controller, underlying_sink_value, callback = underlying_sink.start]() -> WebIDL::ExceptionOr { // Note: callback does not return a promise, so invoke_callback may return an abrupt completion - return TRY(WebIDL::invoke_callback(*callback, underlying_sink_value, controller)); + return TRY(WebIDL::invoke_callback(*callback, underlying_sink_value, WebIDL::ExceptionBehavior::Rethrow, controller)); }); } - // 7. If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which takes an argument chunk and returns the result of invoking underlyingSinkDict["write"] with argument list « chunk, controller » and callback this value underlyingSink. + // 7. If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which takes an argument chunk + // and returns the result of invoking underlyingSinkDict["write"] with argument list « chunk, controller » and + // callback this value underlyingSink. if (underlying_sink.write) { write_algorithm = GC::create_function(realm.heap(), [&realm, controller, underlying_sink_value, callback = underlying_sink.write](JS::Value chunk) { // Note: callback returns a promise, so invoke_callback will never return an abrupt completion @@ -4395,7 +4399,8 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller_from_underly }); } - // 8. If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["close"] with argument list «» and callback this value underlyingSink. + // 8. If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which returns the result of + // invoking underlyingSinkDict["close"] with argument list «» and callback this value underlyingSink. if (underlying_sink.close) { close_algorithm = GC::create_function(realm.heap(), [&realm, underlying_sink_value, callback = underlying_sink.close]() { // Note: callback returns a promise, so invoke_callback will never return an abrupt completion @@ -4404,7 +4409,9 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller_from_underly }); } - // 9. If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSinkDict["abort"] with argument list « reason » and callback this value underlyingSink. + // 9. If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which takes an argument reason + // and returns the result of invoking underlyingSinkDict["abort"] with argument list « reason » and callback this + // value underlyingSink. if (underlying_sink.abort) { abort_algorithm = GC::create_function(realm.heap(), [&realm, underlying_sink_value, callback = underlying_sink.abort](JS::Value reason) { // Note: callback returns a promise, so invoke_callback will never return an abrupt completion From 4010c4643af84adb83803f099f3239bb907053b6 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 10 Apr 2025 09:04:01 -0400 Subject: [PATCH 70/83] LibWeb: Support removing callbacks from AbortSignal This will be needed by Streams. To support this, we now store callbacks in a hash table, keyed by an ID. Callers may use that ID to remove the callback at a later point. --- Libraries/LibWeb/DOM/AbortSignal.cpp | 18 +++++++++++++----- Libraries/LibWeb/DOM/AbortSignal.h | 9 ++++++--- Libraries/LibWeb/DOM/EventTarget.cpp | 2 +- Libraries/LibWeb/Fetch/FetchMethod.cpp | 2 +- Libraries/LibWeb/HTML/CloseWatcher.cpp | 2 +- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Libraries/LibWeb/DOM/AbortSignal.cpp b/Libraries/LibWeb/DOM/AbortSignal.cpp index 633663c0da0..60c24678d20 100644 --- a/Libraries/LibWeb/DOM/AbortSignal.cpp +++ b/Libraries/LibWeb/DOM/AbortSignal.cpp @@ -35,14 +35,22 @@ void AbortSignal::initialize(JS::Realm& realm) } // https://dom.spec.whatwg.org/#abortsignal-add -void AbortSignal::add_abort_algorithm(Function abort_algorithm) +Optional AbortSignal::add_abort_algorithm(Function abort_algorithm) { // 1. If signal is aborted, then return. if (aborted()) - return; + return {}; // 2. Append algorithm to signal’s abort algorithms. - m_abort_algorithms.append(GC::create_function(vm().heap(), move(abort_algorithm))); + m_abort_algorithms.set(++m_next_abort_algorithm_id, GC::create_function(vm().heap(), move(abort_algorithm))); + return m_next_abort_algorithm_id; +} + +// https://dom.spec.whatwg.org/#abortsignal-remove +void AbortSignal::remove_abort_algorithm(AbortAlgorithmID id) +{ + // To remove an algorithm algorithm from an AbortSignal signal, remove algorithm from signal’s abort algorithms. + m_abort_algorithms.remove(id); } // https://dom.spec.whatwg.org/#abortsignal-signal-abort @@ -76,8 +84,8 @@ void AbortSignal::signal_abort(JS::Value reason) // https://dom.spec.whatwg.org/#run-the-abort-steps auto run_the_abort_steps = [](auto& signal) { // 1. For each algorithm in signal’s abort algorithms: run algorithm. - for (auto& algorithm : signal.m_abort_algorithms) - algorithm->function()(); + for (auto const& algorithm : signal.m_abort_algorithms) + algorithm.value->function()(); // 2. Empty signal’s abort algorithms. signal.m_abort_algorithms.clear(); diff --git a/Libraries/LibWeb/DOM/AbortSignal.h b/Libraries/LibWeb/DOM/AbortSignal.h index ac5f518218c..26a654b3476 100644 --- a/Libraries/LibWeb/DOM/AbortSignal.h +++ b/Libraries/LibWeb/DOM/AbortSignal.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -26,7 +27,9 @@ public: virtual ~AbortSignal() override = default; - void add_abort_algorithm(ESCAPING Function); + using AbortAlgorithmID = u64; + Optional add_abort_algorithm(Function); + void remove_abort_algorithm(AbortAlgorithmID); // https://dom.spec.whatwg.org/#dom-abortsignal-aborted // An AbortSignal object is aborted when its abort reason is not undefined. @@ -68,8 +71,8 @@ private: JS::Value m_abort_reason { JS::js_undefined() }; // https://dom.spec.whatwg.org/#abortsignal-abort-algorithms - // FIXME: This should be a set. - Vector>> m_abort_algorithms; + OrderedHashMap>> m_abort_algorithms; + AbortAlgorithmID m_next_abort_algorithm_id { 0 }; // https://dom.spec.whatwg.org/#abortsignal-source-signals // An AbortSignal object has associated source signals (a weak set of AbortSignal objects that the object is dependent on for its aborted state), which is initially empty. diff --git a/Libraries/LibWeb/DOM/EventTarget.cpp b/Libraries/LibWeb/DOM/EventTarget.cpp index 25ae41e08b6..740f4cbd9df 100644 --- a/Libraries/LibWeb/DOM/EventTarget.cpp +++ b/Libraries/LibWeb/DOM/EventTarget.cpp @@ -231,7 +231,7 @@ void EventTarget::add_an_event_listener(DOMEventListener& listener) // 6. If listener’s signal is not null, then add the following abort steps to it: if (listener.signal) { // NOTE: `this` and `listener` are protected by AbortSignal using GC::HeapFunction. - listener.signal->add_abort_algorithm([this, &listener] { + (void)listener.signal->add_abort_algorithm([this, &listener] { // 1. Remove an event listener with eventTarget and listener. remove_an_event_listener(listener); }); diff --git a/Libraries/LibWeb/Fetch/FetchMethod.cpp b/Libraries/LibWeb/Fetch/FetchMethod.cpp index 64531614ee3..9f83cb3696a 100644 --- a/Libraries/LibWeb/Fetch/FetchMethod.cpp +++ b/Libraries/LibWeb/Fetch/FetchMethod.cpp @@ -130,7 +130,7 @@ GC::Ref fetch(JS::VM& vm, RequestInfo const& input, RequestInit })))); // 11. Add the following abort steps to requestObject’s signal: - request_object->signal()->add_abort_algorithm([locally_aborted, request, controller_holder, promise_capability, request_object, response_object, &relevant_realm] { + (void)request_object->signal()->add_abort_algorithm([locally_aborted, request, controller_holder, promise_capability, request_object, response_object, &relevant_realm] { dbgln_if(WEB_FETCH_DEBUG, "Fetch: Request object signal's abort algorithm called"); // 1. Set locallyAborted to true. diff --git a/Libraries/LibWeb/HTML/CloseWatcher.cpp b/Libraries/LibWeb/HTML/CloseWatcher.cpp index 86edb65bbda..8643e8abead 100644 --- a/Libraries/LibWeb/HTML/CloseWatcher.cpp +++ b/Libraries/LibWeb/HTML/CloseWatcher.cpp @@ -65,7 +65,7 @@ WebIDL::ExceptionOr> CloseWatcher::construct_impl(JS::Real } // 3.2 Add the following steps to options["signal"]: - signal->add_abort_algorithm([close_watcher] { + (void)signal->add_abort_algorithm([close_watcher] { // 3.2.1 Destroy closeWatcher. close_watcher->destroy(); }); From eb0a51faf095684e5c2a93205be2b58387072c95 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Apr 2025 12:01:51 -0400 Subject: [PATCH 71/83] LibWeb: Implement ReadableStreamPipeTo according to spec Our existing implementation of stream piping was extremely ad-hoc. It did nothing to handle closed/errored streams, and did not read from or write to streams in a way required by the spec. This new implementation uses a custom JS::Cell to drive the read/write loop. --- .../LibWeb/Streams/AbstractOperations.cpp | 482 ++++++++++++-- .../Streams/ReadableStreamDefaultReader.cpp | 37 +- .../Streams/ReadableStreamDefaultReader.h | 7 +- Libraries/LibWeb/WebIDL/Promise.cpp | 10 +- Libraries/LibWeb/WebIDL/Promise.h | 1 + .../compression-bad-chunks.tentative.any.txt | 2 +- .../streams/encode-bad-chunks.any.txt | 2 +- .../wpt-import/streams/piping/abort.any.txt | 38 ++ .../piping/close-propagation-backward.any.txt | 21 + .../piping/close-propagation-forward.any.txt | 35 + .../piping/error-propagation-backward.any.txt | 40 ++ .../piping/error-propagation-forward.any.txt | 37 + .../streams/piping/flow-control.any.txt | 10 + .../wpt-import/streams/piping/general.any.txt | 19 + .../piping/multiple-propagation.any.txt | 14 + .../streams/readable-byte-streams/tee.any.txt | 2 +- .../streams/writable-streams/aborting.any.txt | 2 +- .../wpt-import/streams/piping/abort.any.html | 16 + .../wpt-import/streams/piping/abort.any.js | 448 +++++++++++++ .../close-propagation-backward.any.html | 15 + .../piping/close-propagation-backward.any.js | 153 +++++ .../piping/close-propagation-forward.any.html | 16 + .../piping/close-propagation-forward.any.js | 589 ++++++++++++++++ .../error-propagation-backward.any.html | 16 + .../piping/error-propagation-backward.any.js | 630 ++++++++++++++++++ .../piping/error-propagation-forward.any.html | 16 + .../piping/error-propagation-forward.any.js | 569 ++++++++++++++++ .../streams/piping/flow-control.any.html | 17 + .../streams/piping/flow-control.any.js | 297 +++++++++ .../streams/piping/general.any.html | 15 + .../wpt-import/streams/piping/general.any.js | 212 ++++++ .../piping/multiple-propagation.any.html | 16 + .../piping/multiple-propagation.any.js | 227 +++++++ 33 files changed, 3926 insertions(+), 85 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js diff --git a/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Libraries/LibWeb/Streams/AbstractOperations.cpp index e34b1ebfbd8..dc3dba99711 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -289,8 +289,383 @@ bool readable_stream_has_default_reader(ReadableStream const& stream) return false; } +// https://streams.spec.whatwg.org/#ref-for-in-parallel +class ReadableStreamPipeTo final : public JS::Cell { + GC_CELL(ReadableStreamPipeTo, JS::Cell); + GC_DECLARE_ALLOCATOR(ReadableStreamPipeTo); + +public: + void process() + { + if (check_for_error_and_close_states()) + return; + + auto ready_promise = m_writer->ready(); + + if (ready_promise && WebIDL::is_promise_fulfilled(*ready_promise)) { + read_chunk(); + return; + } + + auto when_ready = GC::create_function(m_realm->heap(), [this](JS::Value) -> WebIDL::ExceptionOr { + read_chunk(); + return JS::js_undefined(); + }); + + auto shutdown = GC::create_function(heap(), [this](JS::Value) -> WebIDL::ExceptionOr { + check_for_error_and_close_states(); + return JS::js_undefined(); + }); + + if (ready_promise) + WebIDL::react_to_promise(*ready_promise, when_ready, shutdown); + if (auto promise = m_reader->closed()) + WebIDL::react_to_promise(*promise, shutdown, shutdown); + } + + void set_abort_signal(GC::Ref signal, DOM::AbortSignal::AbortSignal::AbortAlgorithmID signal_id) + { + m_signal = signal; + m_signal_id = signal_id; + } + + // https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action + void shutdown_with_action(GC::Ref()>> action, Optional original_error = {}) + { + // 1. If shuttingDown is true, abort these substeps. + if (m_shutting_down) + return; + + // 2. Set shuttingDown to true. + m_shutting_down = true; + + auto on_pending_writes_complete = [this, action, original_error = move(original_error)]() mutable { + HTML::TemporaryExecutionContext execution_context { m_realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 4. Let p be the result of performing action. + auto promise = action->function()(); + + WebIDL::react_to_promise(promise, + // 5. Upon fulfillment of p, finalize, passing along originalError if it was given. + GC::create_function(heap(), [this, original_error = move(original_error)](JS::Value) mutable -> WebIDL::ExceptionOr { + finish(move(original_error)); + return JS::js_undefined(); + }), + + // 6. Upon rejection of p with reason newError, finalize with newError. + GC::create_function(heap(), [this](JS::Value new_error) -> WebIDL::ExceptionOr { + finish(new_error); + return JS::js_undefined(); + })); + }; + + // 3. If dest.[[state]] is "writable" and ! WritableStreamCloseQueuedOrInFlight(dest) is false, + if (m_destination->state() == WritableStream::State::Writable && !writable_stream_close_queued_or_in_flight(m_destination)) { + // 1. If any chunks have been read but not yet written, write them to dest. + write_unwritten_chunks(); + + // 2. Wait until every chunk that has been read has been written (i.e. the corresponding promises have settled). + wait_for_pending_writes_to_complete(move(on_pending_writes_complete)); + } else { + on_pending_writes_complete(); + } + } + + // https://streams.spec.whatwg.org/#rs-pipeTo-shutdown + void shutdown(Optional error = {}) + { + // 1. If shuttingDown is true, abort these substeps. + if (m_shutting_down) + return; + + // 2. Set shuttingDown to true. + m_shutting_down = true; + + auto on_pending_writes_complete = [this, error = move(error)]() mutable { + HTML::TemporaryExecutionContext execution_context { m_realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 4. Finalize, passing along error if it was given. + finish(move(error)); + }; + + // 3. If dest.[[state]] is "writable" and ! WritableStreamCloseQueuedOrInFlight(dest) is false, + if (m_destination->state() == WritableStream::State::Writable && !writable_stream_close_queued_or_in_flight(m_destination)) { + // 1. If any chunks have been read but not yet written, write them to dest. + write_unwritten_chunks(); + + // 2. Wait until every chunk that has been read has been written (i.e. the corresponding promises have settled). + wait_for_pending_writes_to_complete(move(on_pending_writes_complete)); + } else { + on_pending_writes_complete(); + } + } + +private: + ReadableStreamPipeTo( + GC::Ref realm, + GC::Ref promise, + GC::Ref source, + GC::Ref destination, + GC::Ref reader, + GC::Ref writer, + bool prevent_close, + bool prevent_abort, + bool prevent_cancel) + : m_realm(realm) + , m_promise(promise) + , m_source(source) + , m_destination(destination) + , m_reader(reader) + , m_writer(writer) + , m_prevent_close(prevent_close) + , m_prevent_abort(prevent_abort) + , m_prevent_cancel(prevent_cancel) + { + m_reader->set_readable_stream_pipe_to_operation({}, this); + } + + virtual void visit_edges(Cell::Visitor& visitor) override + { + Base::visit_edges(visitor); + visitor.visit(m_realm); + visitor.visit(m_promise); + visitor.visit(m_source); + visitor.visit(m_destination); + visitor.visit(m_reader); + visitor.visit(m_writer); + visitor.visit(m_signal); + visitor.visit(m_pending_writes); + visitor.visit(m_unwritten_chunks); + } + + void read_chunk() + { + // Shutdown must stop activity: if shuttingDown becomes true, the user agent must not initiate further reads from + // reader, and must only perform writes of already-read chunks, as described below. In particular, the user agent + // must check the below conditions before performing any reads or writes, since they might lead to immediate shutdown. + if (check_for_error_and_close_states()) + return; + + auto when_ready = GC::create_function(heap(), [this](JS::Value value) -> WebIDL::ExceptionOr { + auto& vm = this->vm(); + + VERIFY(value.is_object()); + auto& object = value.as_object(); + + auto done = MUST(JS::iterator_complete(vm, object)); + + if (done) { + if (!check_for_error_and_close_states()) + finish(); + } else { + auto chunk = MUST(JS::iterator_value(vm, object)); + m_unwritten_chunks.append(chunk); + + write_chunk(); + process(); + } + + return JS::js_undefined(); + }); + + auto shutdown = GC::create_function(heap(), [this](JS::Value) -> WebIDL::ExceptionOr { + check_for_error_and_close_states(); + return JS::js_undefined(); + }); + + WebIDL::react_to_promise(m_reader->read(), when_ready, shutdown); + + if (auto promise = m_writer->closed()) + WebIDL::react_to_promise(*promise, shutdown, shutdown); + } + + void write_chunk() + { + // Shutdown must stop activity: if shuttingDown becomes true, the user agent must not initiate further reads from + // reader, and must only perform writes of already-read chunks, as described below. In particular, the user agent + // must check the below conditions before performing any reads or writes, since they might lead to immediate shutdown. + if (!m_shutting_down && check_for_error_and_close_states()) + return; + + auto promise = m_writer->write(m_unwritten_chunks.take_first()); + WebIDL::mark_promise_as_handled(promise); + + m_pending_writes.append(promise); + } + + void write_unwritten_chunks() + { + while (!m_unwritten_chunks.is_empty()) + write_chunk(); + } + + void wait_for_pending_writes_to_complete(Function on_complete) + { + auto handler = GC::create_function(heap(), [this, on_complete = move(on_complete)]() { + m_pending_writes.clear(); + on_complete(); + }); + + auto success_steps = [handler](Vector const&) { handler->function()(); }; + auto failure_steps = [handler](JS::Value) { handler->function()(); }; + + WebIDL::wait_for_all(m_realm, m_pending_writes, move(success_steps), move(failure_steps)); + } + + // https://streams.spec.whatwg.org/#rs-pipeTo-finalize + // We call this `finish` instead of `finalize` to avoid conflicts with GC::Cell::finalize. + void finish(Optional error = {}) + { + // 1. Perform ! WritableStreamDefaultWriterRelease(writer). + writable_stream_default_writer_release(m_writer); + + // 2. If reader implements ReadableStreamBYOBReader, perform ! ReadableStreamBYOBReaderRelease(reader). + // 3. Otherwise, perform ! ReadableStreamDefaultReaderRelease(reader). + readable_stream_default_reader_release(m_reader); + + // 4. If signal is not undefined, remove abortAlgorithm from signal. + if (m_signal) + m_signal->remove_abort_algorithm(m_signal_id); + + // 5. If error was given, reject promise with error. + if (error.has_value()) { + WebIDL::reject_promise(m_realm, m_promise, *error); + } + // 6. Otherwise, resolve promise with undefined. + else { + WebIDL::resolve_promise(m_realm, m_promise, JS::js_undefined()); + } + + m_reader->set_readable_stream_pipe_to_operation({}, nullptr); + } + + bool check_for_error_and_close_states() + { + // Error and close states must be propagated: the following conditions must be applied in order. + return m_shutting_down + || check_for_forward_errors() + || check_for_backward_errors() + || check_for_forward_close() + || check_for_backward_close(); + } + + bool check_for_forward_errors() + { + // 1. Errors must be propagated forward: if source.[[state]] is or becomes "errored", then + if (m_source->state() == ReadableStream::State::Errored) { + // 1. If preventAbort is false, shutdown with an action of ! WritableStreamAbort(dest, source.[[storedError]]) + // and with source.[[storedError]]. + if (!m_prevent_abort) { + auto action = GC::create_function(heap(), [this]() { + return writable_stream_abort(m_destination, m_source->stored_error()); + }); + + shutdown_with_action(action, m_source->stored_error()); + } + // 2. Otherwise, shutdown with source.[[storedError]]. + else { + shutdown(m_source->stored_error()); + } + } + + return m_shutting_down; + } + + bool check_for_backward_errors() + { + // 2. Errors must be propagated backward: if dest.[[state]] is or becomes "errored", then + if (m_destination->state() == WritableStream::State::Errored) { + // 1. If preventCancel is false, shutdown with an action of ! ReadableStreamCancel(source, dest.[[storedError]]) + // and with dest.[[storedError]]. + if (!m_prevent_cancel) { + auto action = GC::create_function(heap(), [this]() { + return readable_stream_cancel(m_source, m_destination->stored_error()); + }); + + shutdown_with_action(action, m_destination->stored_error()); + } + // 2. Otherwise, shutdown with dest.[[storedError]]. + else { + shutdown(m_destination->stored_error()); + } + } + + return m_shutting_down; + } + + bool check_for_forward_close() + { + // 3. Closing must be propagated forward: if source.[[state]] is or becomes "closed", then + if (m_source->state() == ReadableStream::State::Closed) { + // 1. If preventClose is false, shutdown with an action of ! WritableStreamDefaultWriterCloseWithErrorPropagation(writer). + if (!m_prevent_close) { + auto action = GC::create_function(heap(), [this]() { + return writable_stream_default_writer_close_with_error_propagation(m_writer); + }); + + shutdown_with_action(action); + } + // 2. Otherwise, shutdown. + else { + shutdown(); + } + } + + return m_shutting_down; + } + + bool check_for_backward_close() + { + // 4. Closing must be propagated backward: if ! WritableStreamCloseQueuedOrInFlight(dest) is true or dest.[[state]] is "closed", then + if (writable_stream_close_queued_or_in_flight(m_destination) || m_destination->state() == WritableStream::State::Closed) { + // 1. Assert: no chunks have been read or written. + + // 2. Let destClosed be a new TypeError. + auto destination_closed = JS::TypeError::create(m_realm, "Destination stream was closed during piping operation"sv); + + // 3. If preventCancel is false, shutdown with an action of ! ReadableStreamCancel(source, destClosed) and with destClosed. + if (!m_prevent_cancel) { + auto action = GC::create_function(heap(), [this, destination_closed]() { + return readable_stream_cancel(m_source, destination_closed); + }); + + shutdown_with_action(action, destination_closed); + } + // 4. Otherwise, shutdown with destClosed. + else { + shutdown(destination_closed); + } + } + + return m_shutting_down; + } + + GC::Ref m_realm; + GC::Ref m_promise; + + GC::Ref m_source; + GC::Ref m_destination; + + GC::Ref m_reader; + GC::Ref m_writer; + + GC::Ptr m_signal; + DOM::AbortSignal::AbortAlgorithmID m_signal_id { 0 }; + + Vector> m_pending_writes; + Vector m_unwritten_chunks; + + bool m_prevent_close { false }; + bool m_prevent_abort { false }; + bool m_prevent_cancel { false }; + + bool m_shutting_down { false }; +}; + +GC_DEFINE_ALLOCATOR(ReadableStreamPipeTo); + // https://streams.spec.whatwg.org/#readable-stream-pipe-to -GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool, bool, bool, GC::Ptr signal) +GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool prevent_close, bool prevent_abort, bool prevent_cancel, GC::Ptr signal) { auto& realm = source.realm(); @@ -300,7 +675,6 @@ GC::Ref readable_stream_pipe_to(ReadableStream& source, Writabl // 4. If signal was not given, let signal be undefined. // 5. Assert: either signal is undefined, or signal implements AbortSignal. - (void)signal; // 6. Assert: ! IsReadableStreamLocked(source) is false. VERIFY(!is_readable_stream_locked(source)); @@ -322,57 +696,81 @@ GC::Ref readable_stream_pipe_to(ReadableStream& source, Writabl // 11. Set source.[[disturbed]] to true. source.set_disturbed(true); - // FIXME: 12. Let shuttingDown be false. + // 12. Let shuttingDown be false. + // NOTE: This is internal to the ReadableStreamPipeTo class. // 13. Let promise be a new promise. auto promise = WebIDL::create_promise(realm); - // FIXME 14. If signal is not undefined, - // 1. Let abortAlgorithm be the following steps: - // 1. Let error be signal’s abort reason. - // 2. Let actions be an empty ordered set. - // 3. If preventAbort is false, append the following action to actions: - // 1. If dest.[[state]] is "writable", return ! WritableStreamAbort(dest, error). - // 2. Otherwise, return a promise resolved with undefined. - // 4. If preventCancel is false, append the following action to actions: - // 1. If source.[[state]] is "readable", return ! ReadableStreamCancel(source, error). - // 2. Otherwise, return a promise resolved with undefined. - // 5. Shutdown with an action consisting of getting a promise to wait for all of the actions in actions, and with error. - // 2. If signal is aborted, perform abortAlgorithm and return promise. - // 3. Add abortAlgorithm to signal. + auto operation = realm.heap().allocate(realm, promise, source, dest, reader, writer, prevent_close, prevent_abort, prevent_cancel); - // 15. In parallel but not really; see #905, using reader and writer, read all chunks from source and write them to - // dest. Due to the locking provided by the reader and writer, the exact manner in which this happens is not - // observable to author code, and so there is flexibility in how this is done. The following constraints apply - // regardless of the exact algorithm used: - // - Public API must not be used: while reading or writing, or performing any of the operations below, the - // JavaScript-modifiable reader, writer, and stream APIs (i.e. methods on the appropriate prototypes) must not - // be used. Instead, the streams must be manipulated directly. + // 14. If signal is not undefined, + if (signal) { + // 1. Let abortAlgorithm be the following steps: + auto abort_algorithm = [&realm, operation, source = GC::Ref { source }, dest = GC::Ref { dest }, prevent_abort, prevent_cancel, signal]() { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; - // FIXME: Currently a naive implementation that uses ReadableStreamDefaultReader::read_all_chunks() to read all chunks - // from the source and then through the callback success_steps writes those chunks to the destination. - auto chunk_steps = GC::create_function(realm.heap(), [&realm, writer](JS::Value chunk) { - auto promise = writable_stream_default_writer_write(writer, chunk); - WebIDL::resolve_promise(realm, promise, JS::js_undefined()); - }); + // 1. Let error be signal’s abort reason. + auto error = signal->reason(); - auto success_steps = GC::create_function(realm.heap(), [promise, &realm, reader, writer]() { - // Make sure we close the acquired writer. - WebIDL::resolve_promise(realm, writable_stream_default_writer_close(*writer), JS::js_undefined()); - readable_stream_default_reader_release(*reader); + // 2. Let actions be an empty ordered set. + GC::Ptr()>> abort_destination; + GC::Ptr()>> cancel_source; - WebIDL::resolve_promise(realm, promise, JS::js_undefined()); - }); + // 3. If preventAbort is false, append the following action to actions: + if (!prevent_abort) { + abort_destination = GC::create_function(realm.heap(), [&realm, dest, error]() { + // 1. If dest.[[state]] is "writable", return ! WritableStreamAbort(dest, error). + if (dest->state() == WritableStream::State::Writable) + return writable_stream_abort(dest, error); - auto failure_steps = GC::create_function(realm.heap(), [promise, &realm, reader, writer](JS::Value error) { - // Make sure we close the acquired writer. - WebIDL::resolve_promise(realm, writable_stream_default_writer_close(*writer), JS::js_undefined()); - readable_stream_default_reader_release(*reader); + // 2. Otherwise, return a promise resolved with undefined. + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + }); + } - WebIDL::reject_promise(realm, promise, error); - }); + // 4. If preventCancel is false, append the following action action to actions: + if (!prevent_cancel) { + cancel_source = GC::create_function(realm.heap(), [&realm, source, error]() { + // 1. If source.[[state]] is "readable", return ! ReadableStreamCancel(source, error). + if (source->state() == ReadableStream::State::Readable) + return readable_stream_cancel(source, error); - reader->read_all_chunks(chunk_steps, success_steps, failure_steps); + // 2. Otherwise, return a promise resolved with undefined. + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + }); + } + + // 5. Shutdown with an action consisting of getting a promise to wait for all of the actions in actions, and with error. + auto action = GC::create_function(realm.heap(), [&realm, abort_destination, cancel_source]() { + GC::RootVector> actions(realm.heap()); + + if (abort_destination) + actions.append(abort_destination->function()()); + if (cancel_source) + actions.append(cancel_source->function()()); + + return WebIDL::get_promise_for_wait_for_all(realm, actions); + }); + + operation->shutdown_with_action(action, error); + }; + + // 2. If signal is aborted, perform abortAlgorithm and return promise. + if (signal->aborted()) { + abort_algorithm(); + return promise; + } + + // 3. Add abortAlgorithm to signal. + auto signal_id = signal->add_abort_algorithm(move(abort_algorithm)); + operation->set_abort_signal(*signal, signal_id.value()); + } + + // 15. In parallel (but not really; see #905), using reader and writer, read all chunks from source and write them + // to dest. Due to the locking provided by the reader and writer, the exact manner in which this happens is not + // observable to author code, and so there is flexibility in how this is done. + operation->process(); // 16. Return promise. return promise; diff --git a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp index 8ab80faecb1..1aa1bde0560 100644 --- a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp +++ b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp @@ -66,6 +66,7 @@ void ReadableStreamDefaultReader::visit_edges(Cell::Visitor& visitor) ReadableStreamGenericReaderMixin::visit_edges(visitor); for (auto& request : m_read_requests) visitor.visit(request); + visitor.visit(m_readable_stream_pipe_to_operation); } // https://streams.spec.whatwg.org/#read-loop @@ -212,42 +213,6 @@ void ReadableStreamDefaultReader::read_all_bytes(GC::Ref chunk_steps, GC::Ref success_steps, GC::Ref failure_steps) -{ - // AD-HOC: Some spec steps direct us to "read all chunks" from a stream, but there isn't an AO defined to do that. - // We implement those steps by continuously making default read requests, which is an identity transformation, - // with a custom callback to receive each chunk that is read. This is done until the controller signals - // that there are no more chunks to consume. - // This function is based on "read_all_bytes" above. - auto promise_capability = read(); - - WebIDL::react_to_promise( - promise_capability, - GC::create_function(heap(), [this, chunk_steps, success_steps, failure_steps](JS::Value value) -> WebIDL::ExceptionOr { - auto& vm = this->vm(); - - VERIFY(value.is_object()); - auto& value_object = value.as_object(); - - auto done = MUST(JS::iterator_complete(vm, value_object)); - - if (!done) { - auto chunk = MUST(JS::iterator_value(vm, value_object)); - chunk_steps->function()(chunk); - - read_all_chunks(chunk_steps, success_steps, failure_steps); - } else { - success_steps->function()(); - } - - return JS::js_undefined(); - }), - GC::create_function(heap(), [failure_steps](JS::Value error) -> WebIDL::ExceptionOr { - failure_steps->function()(error); - return JS::js_undefined(); - })); -} - // FIXME: This function is a promise-based wrapper around "read all bytes". The spec changed this function to not use promises // in https://github.com/whatwg/streams/commit/f894acdd417926a2121710803cef593e15127964 - however, it seems that the // FileAPI blob specification has not been updated to match, see: https://github.com/w3c/FileAPI/issues/187. diff --git a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h index 8e37bf476e7..de71cb17c39 100644 --- a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h +++ b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h @@ -20,6 +20,8 @@ struct ReadableStreamReadResult { bool done; }; +class ReadableStreamPipeTo; + class ReadRequest : public JS::Cell { GC_CELL(ReadRequest, JS::Cell); @@ -91,13 +93,14 @@ public: void read_a_chunk(Fetch::Infrastructure::IncrementalReadLoopReadRequest& read_request); void read_all_bytes(GC::Ref, GC::Ref); - void read_all_chunks(GC::Ref, GC::Ref, GC::Ref); GC::Ref read_all_bytes_deprecated(); void release_lock(); SinglyLinkedList>& read_requests() { return m_read_requests; } + void set_readable_stream_pipe_to_operation(Badge, GC::Ptr readable_stream_pipe_to_operation) { m_readable_stream_pipe_to_operation = readable_stream_pipe_to_operation; } + private: explicit ReadableStreamDefaultReader(JS::Realm&); @@ -106,6 +109,8 @@ private: virtual void visit_edges(Cell::Visitor&) override; SinglyLinkedList> m_read_requests; + + GC::Ptr m_readable_stream_pipe_to_operation; }; } diff --git a/Libraries/LibWeb/WebIDL/Promise.cpp b/Libraries/LibWeb/WebIDL/Promise.cpp index c2bbb0ccbb6..df5733d9db0 100644 --- a/Libraries/LibWeb/WebIDL/Promise.cpp +++ b/Libraries/LibWeb/WebIDL/Promise.cpp @@ -175,8 +175,14 @@ GC::Ref upon_rejection(Promise const& promise, GC::Ref s void mark_promise_as_handled(Promise const& promise) { // To mark as handled a Promise promise, set promise.[[Promise]].[[PromiseIsHandled]] to true. - auto promise_object = as(promise.promise().ptr()); - promise_object->set_is_handled(); + auto& promise_object = as(*promise.promise()); + promise_object.set_is_handled(); +} + +bool is_promise_fulfilled(Promise const& promise) +{ + auto const& promise_object = as(*promise.promise()); + return promise_object.state() == JS::Promise::State::Fulfilled; } struct WaitForAllResults : JS::Cell { diff --git a/Libraries/LibWeb/WebIDL/Promise.h b/Libraries/LibWeb/WebIDL/Promise.h index 82e2fa30b5d..6fdfbb49799 100644 --- a/Libraries/LibWeb/WebIDL/Promise.h +++ b/Libraries/LibWeb/WebIDL/Promise.h @@ -29,6 +29,7 @@ GC::Ref react_to_promise(Promise const&, GC::Ptr on_fulf GC::Ref upon_fulfillment(Promise const&, GC::Ref); GC::Ref upon_rejection(Promise const&, GC::Ref); void mark_promise_as_handled(Promise const&); +bool is_promise_fulfilled(Promise const&); void wait_for_all(JS::Realm&, Vector> const& promises, Function const&)> success_steps, Function failure_steps); GC::Ref get_promise_for_wait_for_all(JS::Realm&, Vector> const& promises); diff --git a/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt b/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt index 2c8da675484..8ce0f65441d 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 21 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt index db36b3a6af6..ea1b1e6c0d0 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 6 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt new file mode 100644 index 00000000000..9ceba4b0d72 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt @@ -0,0 +1,38 @@ +Harness status: OK + +Found 33 tests + +33 Pass +Pass a signal argument 'null' should cause pipeTo() to reject +Pass a signal argument 'AbortSignal' should cause pipeTo() to reject +Pass a signal argument 'true' should cause pipeTo() to reject +Pass a signal argument '-1' should cause pipeTo() to reject +Pass a signal argument '[object AbortSignal]' should cause pipeTo() to reject +Pass an aborted signal should cause the writable stream to reject with an AbortError +Pass (reason: 'null') all the error objects should be the same object +Pass (reason: 'undefined') all the error objects should be the same object +Pass (reason: 'error1: error1') all the error objects should be the same object +Pass preventCancel should prevent canceling the readable +Pass preventAbort should prevent aborting the readable +Pass preventCancel and preventAbort should prevent canceling the readable and aborting the readable +Pass (reason: 'null') abort should prevent further reads +Pass (reason: 'undefined') abort should prevent further reads +Pass (reason: 'error1: error1') abort should prevent further reads +Pass (reason: 'null') all pending writes should complete on abort +Pass (reason: 'undefined') all pending writes should complete on abort +Pass (reason: 'error1: error1') all pending writes should complete on abort +Pass (reason: 'null') underlyingSource.cancel() should called when abort, even with pending pull +Pass (reason: 'undefined') underlyingSource.cancel() should called when abort, even with pending pull +Pass (reason: 'error1: error1') underlyingSource.cancel() should called when abort, even with pending pull +Pass a rejection from underlyingSource.cancel() should be returned by pipeTo() +Pass a rejection from underlyingSink.abort() should be returned by pipeTo() +Pass a rejection from underlyingSink.abort() should be preferred to one from underlyingSource.cancel() +Pass abort signal takes priority over closed readable +Pass abort signal takes priority over errored readable +Pass abort signal takes priority over closed writable +Pass abort signal takes priority over errored writable +Pass abort should do nothing after the readable is closed +Pass abort should do nothing after the readable is errored +Pass abort should do nothing after the readable is errored, even with pending writes +Pass abort should do nothing after the writable is errored +Pass pipeTo on a teed readable byte stream should only be aborted when both branches are aborted \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt new file mode 100644 index 00000000000..94f417830b1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt @@ -0,0 +1,21 @@ +Harness status: OK + +Found 16 tests + +16 Pass +Pass Closing must be propagated backward: starts closed; preventCancel omitted; fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel omitted; rejected cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = undefined (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = null (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = false (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = 0 (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = -0 (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = NaN (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = true (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = a (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = 1 (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = Symbol() (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = [object Object] (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true +Pass Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true, preventClose = true \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt new file mode 100644 index 00000000000..8911034e974 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt @@ -0,0 +1,35 @@ +Harness status: OK + +Found 30 tests + +30 Pass +Pass Closing must be propagated forward: starts closed; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: starts closed; preventClose = undefined (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = null (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = false (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = 0 (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = -0 (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = NaN (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = true (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = a (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = 1 (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = Symbol() (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = [object Object] (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true +Pass Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true, preventCancel = true +Pass Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: becomes closed asynchronously; preventClose = true +Pass Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; preventClose = true +Pass Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: becomes closed after one chunk; preventClose = true +Pass Closing must be propagated forward: shutdown must not occur until the final write completes +Pass Closing must be propagated forward: shutdown must not occur until the final write completes; preventClose = true +Pass Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write +Pass Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write; preventClose = true +Pass Closing must be propagated forward: erroring the writable while flushing pending writes should error pipeTo \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt new file mode 100644 index 00000000000..47969a0fed1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt @@ -0,0 +1,40 @@ +Harness status: OK + +Found 35 tests + +35 Pass +Pass Errors must be propagated backward: starts errored; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = undefined (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = null (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = false (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = 0 (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = -0 (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = NaN (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = a (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = 1 (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = Symbol() (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = [object Object] (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write, preventCancel = true; preventAbort = true +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true, preventAbort = true, preventClose = true +Pass Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write; preventCancel = true +Pass Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = false; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = false; rejected cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = true +Pass Errors must be propagated backward: becomes errored after piping; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored after piping; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored after piping; preventCancel = true +Pass Errors must be propagated backward: becomes errored after piping due to last write; source is closed; preventCancel omitted (but cancel is never called) +Pass Errors must be propagated backward: becomes errored after piping due to last write; source is closed; preventCancel = true +Pass Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = false; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = false; rejected cancel promise +Pass Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = true +Pass Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored before piping via abort; preventCancel = true +Pass Errors must be propagated backward: erroring via the controller errors once pending write completes \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt new file mode 100644 index 00000000000..1406d4298aa --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt @@ -0,0 +1,37 @@ +Harness status: OK + +Found 32 tests + +32 Pass +Pass Errors must be propagated forward: starts errored; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = undefined (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = null (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = false (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = 0 (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = -0 (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = NaN (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = true (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = a (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = 1 (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = Symbol() (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = [object Object] (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true +Pass Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true, preventClose = true +Pass Errors must be propagated forward: becomes errored while empty; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored while empty; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored while empty; preventAbort = true +Pass Errors must be propagated forward: becomes errored while empty; dest never desires chunks; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored while empty; dest never desires chunks; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored while empty; dest never desires chunks; preventAbort = true +Pass Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; preventAbort = true +Pass Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; preventAbort = true +Pass Errors must be propagated forward: shutdown must not occur until the final write completes +Pass Errors must be propagated forward: shutdown must not occur until the final write completes; preventAbort = true +Pass Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write +Pass Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write; preventAbort = true \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt new file mode 100644 index 00000000000..eaa579cda26 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt @@ -0,0 +1,10 @@ +Harness status: OK + +Found 5 tests + +5 Pass +Pass Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks +Pass Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks, but then does +Pass Piping from an empty ReadableStream into a WritableStream that does not desire chunks, but then the readable stream becomes non-empty and the writable stream starts desiring chunks +Pass Piping from a ReadableStream to a WritableStream that desires more chunks before finishing with previous ones +Pass Piping to a WritableStream that does not consume the writes fast enough exerts backpressure on the ReadableStream \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt new file mode 100644 index 00000000000..8baf203c193 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt @@ -0,0 +1,19 @@ +Harness status: OK + +Found 14 tests + +14 Pass +Pass Piping must lock both the ReadableStream and WritableStream +Pass Piping finishing must unlock both the ReadableStream and WritableStream +Pass pipeTo must check the brand of its ReadableStream this value +Pass pipeTo must check the brand of its WritableStream argument +Pass pipeTo must fail if the ReadableStream is locked, and not lock the WritableStream +Pass pipeTo must fail if the WritableStream is locked, and not lock the ReadableStream +Pass Piping from a ReadableStream from which lots of chunks are synchronously readable +Pass Piping from a ReadableStream for which a chunk becomes asynchronously readable after the pipeTo +Pass an undefined rejection from pull should cause pipeTo() to reject when preventAbort is true +Pass an undefined rejection from pull should cause pipeTo() to reject when preventAbort is false +Pass an undefined rejection from write should cause pipeTo() to reject when preventCancel is true +Pass an undefined rejection from write should cause pipeTo() to reject when preventCancel is false +Pass pipeTo() should reject if an option getter grabs a writer +Pass pipeTo() promise should resolve if null is passed \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt new file mode 100644 index 00000000000..212f4762aa8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt @@ -0,0 +1,14 @@ +Harness status: OK + +Found 9 tests + +9 Pass +Pass Piping from an errored readable stream to an erroring writable stream +Pass Piping from an errored readable stream to an errored writable stream +Pass Piping from an errored readable stream to an erroring writable stream; preventAbort = true +Pass Piping from an errored readable stream to an errored writable stream; preventAbort = true +Pass Piping from an errored readable stream to a closing writable stream +Pass Piping from an errored readable stream to a closed writable stream +Pass Piping from a closed readable stream to an erroring writable stream +Pass Piping from a closed readable stream to an errored writable stream +Pass Piping from a closed readable stream to a closed writable stream \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt index ea4f794f5cf..d0d6f306af7 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 39 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt index 3d42dd06767..db9690d7e18 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 62 tests diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html new file mode 100644 index 00000000000..f98677456d1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js new file mode 100644 index 00000000000..e813b017769 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js @@ -0,0 +1,448 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/recording-streams.js +// META: script=../resources/test-utils.js +'use strict'; + +// Tests for the use of pipeTo with AbortSignal. +// There is some extra complexity to avoid timeouts in environments where abort is not implemented. + +const error1 = new Error('error1'); +error1.name = 'error1'; +const error2 = new Error('error2'); +error2.name = 'error2'; + +const errorOnPull = { + pull(controller) { + // This will cause the test to error if pipeTo abort is not implemented. + controller.error('failed to abort'); + } +}; + +// To stop pull() being called immediately when the stream is created, we need to set highWaterMark to 0. +const hwm0 = { highWaterMark: 0 }; + +for (const invalidSignal of [null, 'AbortSignal', true, -1, Object.create(AbortSignal.prototype)]) { + promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { signal: invalidSignal }), 'pipeTo should reject') + .then(() => { + assert_equals(rs.events.length, 0, 'no ReadableStream methods should have been called'); + assert_equals(ws.events.length, 0, 'no WritableStream methods should have been called'); + }); + }, `a signal argument '${invalidSignal}' should cause pipeTo() to reject`); +} + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject') + .then(() => Promise.all([ + rs.getReader().closed, + promise_rejects_dom(t, 'AbortError', ws.getWriter().closed, 'writer.closed should reject') + ])) + .then(() => { + assert_equals(rs.events.length, 2, 'cancel should have been called'); + assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); + assert_equals(rs.events[1].name, 'AbortError', 'the argument to cancel should be an AbortError'); + assert_equals(rs.events[1].constructor.name, 'DOMException', + 'the argument to cancel should be a DOMException'); + }); +}, 'an aborted signal should cause the writable stream to reject with an AbortError'); + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(reason); + const pipeToPromise = rs.pipeTo(ws, { signal }); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + await rs.getReader().closed; + await promise_rejects_exactly(t, error, ws.getWriter().closed, 'the writable should be errored with the same object'); + assert_equals(signal.reason, error, 'signal.reason should be error'), + assert_equals(rs.events.length, 2, 'cancel should have been called'); + assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); + assert_equals(rs.events[1], error, 'the readable should be canceled with the same object'); + }, `(reason: '${reason}') all the error objects should be the same object`); +} + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true }), 'pipeTo should reject') + .then(() => assert_equals(rs.events.length, 0, 'cancel should not be called')); +}, 'preventCancel should prevent canceling the readable'); + +promise_test(t => { + const rs = new ReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventAbort: true }), 'pipeTo should reject') + .then(() => { + assert_equals(ws.events.length, 0, 'writable should not have been aborted'); + return ws.getWriter().ready; + }); +}, 'preventAbort should prevent aborting the readable'); + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true, preventAbort: true }), + 'pipeTo should reject') + .then(() => { + assert_equals(rs.events.length, 0, 'cancel should not be called'); + assert_equals(ws.events.length, 0, 'writable should not have been aborted'); + return ws.getWriter().ready; + }); +}, 'preventCancel and preventAbort should prevent canceling the readable and aborting the readable'); + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + const rs = new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const ws = recordingWritableStream({ + write() { + abortController.abort(reason); + } + }); + const pipeToPromise = rs.pipeTo(ws, { signal }); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + assert_equals(signal.reason, error, 'signal.reason should be error'); + assert_equals(ws.events.length, 4, 'only chunk "a" should have been written'); + assert_array_equals(ws.events.slice(0, 3), ['write', 'a', 'abort'], 'events should match'); + assert_equals(ws.events[3], error, 'abort reason should be error'); + }, `(reason: '${reason}') abort should prevent further reads`); +} + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + c.enqueue('a'); + c.enqueue('b'); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + let resolveWrite; + const writePromise = new Promise(resolve => { + resolveWrite = resolve; + }); + const ws = recordingWritableStream({ + write() { + return writePromise; + } + }, new CountQueuingStrategy({ highWaterMark: Infinity })); + const pipeToPromise = rs.pipeTo(ws, { signal }); + await delay(0); + await abortController.abort(reason); + await readController.close(); // Make sure the test terminates when signal is not implemented. + await resolveWrite(); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + assert_equals(signal.reason, error, 'signal.reason should be error'); + assert_equals(ws.events.length, 6, 'chunks "a" and "b" should have been written'); + assert_array_equals(ws.events.slice(0, 5), ['write', 'a', 'write', 'b', 'abort'], 'events should match'); + assert_equals(ws.events[5], error, 'abort reason should be error'); + }, `(reason: '${reason}') all pending writes should complete on abort`); +} + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + let rejectPull; + const pullPromise = new Promise((_, reject) => { + rejectPull = reject; + }); + let rejectCancel; + const cancelPromise = new Promise((_, reject) => { + rejectCancel = reject; + }); + const rs = recordingReadableStream({ + async pull() { + await Promise.race([ + pullPromise, + cancelPromise, + ]); + }, + cancel(reason) { + rejectCancel(reason); + }, + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal }); + pipeToPromise.catch(() => {}); // Prevent unhandled rejection. + await delay(0); + abortController.abort(reason); + rejectPull('should not catch pull rejection'); + await delay(0); + assert_equals(rs.eventsWithoutPulls.length, 2, 'cancel should have been called'); + assert_equals(rs.eventsWithoutPulls[0], 'cancel', 'first event should be cancel'); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + }, `(reason: '${reason}') underlyingSource.cancel() should called when abort, even with pending pull`); +} + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + }, + cancel() { + return Promise.reject(error1); + } + }, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'a rejection from underlyingSource.cancel() should be returned by pipeTo()'); + +promise_test(t => { + const rs = new ReadableStream(errorOnPull, hwm0); + const ws = new WritableStream({ + abort() { + return Promise.reject(error1); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'a rejection from underlyingSink.abort() should be returned by pipeTo()'); + +promise_test(t => { + const events = []; + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + }, + cancel() { + events.push('cancel'); + return Promise.reject(error1); + } + }, hwm0); + const ws = new WritableStream({ + abort() { + events.push('abort'); + return Promise.reject(error2); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error2, rs.pipeTo(ws, { signal }), 'pipeTo should reject') + .then(() => assert_array_equals(events, ['abort', 'cancel'], 'abort() should be called before cancel()')); +}, 'a rejection from underlyingSink.abort() should be preferred to one from underlyingSource.cancel()'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over closed readable'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.error(error1); + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over errored readable'); + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + } + }, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + const writer = ws.getWriter(); + return writer.close().then(() => { + writer.releaseLock(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); + }); +}, 'abort signal takes priority over closed writable'); + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + } + }, hwm0); + const ws = new WritableStream({ + start(controller) { + controller.error(error1); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over errored writable'); + +promise_test(() => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventClose: true }); + readController.close(); + return Promise.resolve().then(() => { + abortController.abort(); + return pipeToPromise; + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is closed'); + +promise_test(t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); + readController.error(error1); + return Promise.resolve().then(() => { + abortController.abort(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is errored'); + +promise_test(t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + let resolveWrite; + const writePromise = new Promise(resolve => { + resolveWrite = resolve; + }); + const ws = new WritableStream({ + write() { + readController.error(error1); + return writePromise; + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); + readController.enqueue('a'); + return delay(0).then(() => { + abortController.abort(); + resolveWrite(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is errored, even with pending writes'); + +promise_test(t => { + const rs = recordingReadableStream({ + pull(controller) { + return delay(0).then(() => controller.close()); + } + }); + let writeController; + const ws = new WritableStream({ + start(c) { + writeController = c; + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventCancel: true }); + return Promise.resolve().then(() => { + writeController.error(error1); + return Promise.resolve(); + }).then(() => { + abortController.abort(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => { + assert_array_equals(rs.events, ['pull'], 'cancel should not have been called'); + }); +}, 'abort should do nothing after the writable is errored'); + +promise_test(async t => { + const rs = new ReadableStream({ + pull(c) { + c.enqueue(new Uint8Array([])); + }, + type: "bytes", + }); + const ws = new WritableStream(); + const [first, second] = rs.tee(); + + let aborted = false; + first.pipeTo(ws, { signal: AbortSignal.abort() }).catch(() => { + aborted = true; + }); + await delay(0); + assert_true(!aborted, "pipeTo should not resolve yet"); + await second.cancel(); + await delay(0); + assert_true(aborted, "pipeTo should be aborted now"); +}, "pipeTo on a teed readable byte stream should only be aborted when both branches are aborted"); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html new file mode 100644 index 00000000000..38e4080afff --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js new file mode 100644 index 00000000000..25bd475ed13 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js @@ -0,0 +1,153 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err.name, 'TypeError', 'the promise must reject with a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + } + ); + +}, 'Closing must be propagated backward: starts closed; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + // Our recording streams do not deal well with errors generated by the system, so give them some help + let recordedError; + const rs = recordingReadableStream({ + cancel(cancelErr) { + recordedError = cancelErr; + throw error1; + } + }); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_equals(recordedError.name, 'TypeError', 'the cancel reason must be a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', recordedError]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel omitted; rejected cancel promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws, { preventCancel: falsy }).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err.name, 'TypeError', 'the promise must reject with a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + } + ); + + }, `Closing must be propagated backward: starts closed; preventCancel = ${stringVersion} (falsy); fulfilled cancel ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { preventCancel: truthy })).then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + + }, `Closing must be propagated backward: starts closed; preventCancel = ${String(truthy)} (truthy)`); +} + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { preventCancel: true, preventAbort: true })) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, + rs.pipeTo(ws, { preventCancel: true, preventAbort: true, preventClose: true })) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true, preventClose ' + + '= true'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html new file mode 100644 index 00000000000..eea048b3cac --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js new file mode 100644 index 00000000000..0ec94f80abf --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js @@ -0,0 +1,589 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: starts closed; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: starts closed; preventClose omitted; rejected close promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: falsy }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + + }, `Closing must be propagated forward: starts closed; preventClose = ${stringVersion} (falsy); fulfilled close ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: truthy }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + + }, `Closing must be propagated forward: starts closed; preventClose = ${String(truthy)} (truthy)`); +} + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: true, preventAbort: true }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true'); + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: true, preventAbort: true, preventCancel: true }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true, preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose = true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.close(); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'close' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'close']); + }); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws, { preventClose: true }).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.close(); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the chunk must have been written, but close must not have happened'); + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the chunk must have been written, but close must not have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; preventClose = true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but close must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.close(); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but close must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close'], + 'all chunks must have been written and close must have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws, { preventClose: true }).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but close must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.close(); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but close must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'all chunks must have been written, but close must not have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write; preventClose = true'); + + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + let rejectWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise((resolve, reject) => { + rejectWritePromise = reject; + }); + } + }, { highWaterMark: 3 }); + const pipeToPromise = rs.pipeTo(ws); + return delay(0).then(() => { + rejectWritePromise(error1); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['write', 'a']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed, 'ws should be errored') + ]); + }); +}, 'Closing must be propagated forward: erroring the writable while flushing pending writes should error pipeTo'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html new file mode 100644 index 00000000000..aa92999ebf1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js new file mode 100644 index 00000000000..f786469d6c1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js @@ -0,0 +1,630 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + start() { + return Promise.reject(error1); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: starts errored; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; ' + + 'fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; rejected ' + + 'cancel promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: falsy }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + + }, `Errors must be propagated backward: becomes errored before piping due to write; preventCancel = ` + + `${stringVersion} (falsy); fulfilled cancel promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: truthy }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + + }, `Errors must be propagated backward: becomes errored before piping due to write; preventCancel = ` + + `${String(truthy)} (truthy)`); +} + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true, preventAbort: true }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write, preventCancel = true; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true, preventAbort: true, preventClose: true }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true, ' + + 'preventAbort = true, preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; fulfilled ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + }, + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; rejected ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = ' + + 'false; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + }, + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = ' + + 'false; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel omitted; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + controller.close(); + } + }); + + const ws = recordingWritableStream({ + write(chunk) { + if (chunk === 'c') { + return Promise.reject(error1); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c']); + }); + +}, 'Errors must be propagated backward: becomes errored after piping due to last write; source is closed; ' + + 'preventCancel omitted (but cancel is never called)'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + controller.close(); + } + }); + + const ws = recordingWritableStream({ + write(chunk) { + if (chunk === 'c') { + return Promise.reject(error1); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c']); + }); + +}, 'Errors must be propagated backward: becomes errored after piping due to last write; source is closed; ' + + 'preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'false; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'false; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return rs.pipeTo(ws).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err, error1, 'the promise must reject with error1'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['abort', error1]); + } + ); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; fulfilled ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error') + .then(() => { + return ws.getWriter().closed.then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err, error1, 'the promise must reject with error1'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['abort', error1]); + } + ); + }); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; rejected ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true })).then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + return flushAsyncEvents(); + } + }); + + const pipePromise = rs.pipeTo(ws); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + ws.controller.error(error1); + + return promise_rejects_exactly(t, error1, pipePromise); + }).then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a']); + }); + +}, 'Errors must be propagated backward: erroring via the controller errors once pending write completes'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html new file mode 100644 index 00000000000..dcf0443d4c9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js new file mode 100644 index 00000000000..e9260f9ea22 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js @@ -0,0 +1,569 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = false; rejected abort promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: falsy }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + + }, `Errors must be propagated forward: starts errored; preventAbort = ${stringVersion} (falsy); fulfilled abort ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: truthy }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + + }, `Errors must be propagated forward: starts errored; preventAbort = ${String(truthy)} (truthy)`); +} + + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true, preventCancel: true }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true, preventCancel: true, preventClose: true }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true, preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws)).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + rs.controller.error(error1); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'abort', error1]); + }); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true })).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + rs.controller.error(error1); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws)).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but abort must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.error(error1); + resolveWritePromise(); + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but abort must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'abort', error1], + 'all chunks must have been written and abort must have happened'); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true })).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but abort must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.error(error1); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but abort must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'all chunks must have been written, but abort must not have happened'); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write; preventAbort = true'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html new file mode 100644 index 00000000000..8afa4be6159 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html @@ -0,0 +1,17 @@ + + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js new file mode 100644 index 00000000000..e2318da375a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js @@ -0,0 +1,297 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + } + }); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws, { preventCancel: true }); + + // Wait and make sure it doesn't do any reading. + return flushAsyncEvents().then(() => { + ws.controller.error(error1); + }) + .then(() => promise_rejects_exactly(t, error1, pipePromise, 'pipeTo must reject with the same error')) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }) + .then(() => readableStreamToArray(rs)) + .then(chunksNotPreviouslyRead => { + assert_array_equals(chunksNotPreviouslyRead, ['a', 'b']); + }); + +}, 'Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks'); + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('b'); + controller.close(); + } + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }); + + const writer = ws.getWriter(); + const firstWritePromise = writer.write('a'); + assert_equals(writer.desiredSize, 0, 'after writing the writer\'s desiredSize must be 0'); + writer.releaseLock(); + + // firstWritePromise won't settle until we call resolveWritePromise. + + const pipePromise = rs.pipeTo(ws); + + return flushAsyncEvents().then(() => resolveWritePromise()) + .then(() => Promise.all([firstWritePromise, pipePromise])) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close']); + }); + +}, 'Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks, but then does'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }); + + const writer = ws.getWriter(); + writer.write('a'); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); + assert_equals(writer.desiredSize, 0, 'after writing the writer\'s desiredSize must be 0'); + writer.releaseLock(); + + const pipePromise = rs.pipeTo(ws); + + rs.controller.enqueue('b'); + resolveWritePromise(); + rs.controller.close(); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close']); + }); + }); + +}, 'Piping from an empty ReadableStream into a WritableStream that does not desire chunks, but then the readable ' + + 'stream becomes non-empty and the writable stream starts desiring chunks'); + +promise_test(() => { + const unreadChunks = ['b', 'c', 'd']; + + const rs = recordingReadableStream({ + pull(controller) { + controller.enqueue(unreadChunks.shift()); + if (unreadChunks.length === 0) { + controller.close(); + } + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }, new CountQueuingStrategy({ highWaterMark: 3 })); + + const writer = ws.getWriter(); + const firstWritePromise = writer.write('a'); + assert_equals(writer.desiredSize, 2, 'after writing the writer\'s desiredSize must be 2'); + writer.releaseLock(); + + // firstWritePromise won't settle until we call resolveWritePromise. + + const pipePromise = rs.pipeTo(ws); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); + assert_equals(unreadChunks.length, 1, 'chunks should continue to be enqueued until the HWM is reached'); + }).then(() => resolveWritePromise()) + .then(() => Promise.all([firstWritePromise, pipePromise])) + .then(() => { + assert_array_equals(rs.events, ['pull', 'pull', 'pull']); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b','write', 'c','write', 'd', 'close']); + }); + +}, 'Piping from a ReadableStream to a WritableStream that desires more chunks before finishing with previous ones'); + +class StepTracker { + constructor() { + this.waiters = []; + this.wakers = []; + } + + // Returns promise which resolves when step `n` is reached. Also schedules step n + 1 to happen shortly after the + // promise is resolved. + waitThenAdvance(n) { + if (this.waiters[n] === undefined) { + this.waiters[n] = new Promise(resolve => { + this.wakers[n] = resolve; + }); + this.waiters[n] + .then(() => flushAsyncEvents()) + .then(() => { + if (this.wakers[n + 1] !== undefined) { + this.wakers[n + 1](); + } + }); + } + if (n == 0) { + this.wakers[0](); + } + return this.waiters[n]; + } +} + +promise_test(() => { + const steps = new StepTracker(); + const desiredSizes = []; + const rs = recordingReadableStream({ + start(controller) { + steps.waitThenAdvance(1).then(() => enqueue('a')); + steps.waitThenAdvance(3).then(() => enqueue('b')); + steps.waitThenAdvance(5).then(() => enqueue('c')); + steps.waitThenAdvance(7).then(() => enqueue('d')); + steps.waitThenAdvance(11).then(() => controller.close()); + + function enqueue(chunk) { + controller.enqueue(chunk); + desiredSizes.push(controller.desiredSize); + } + } + }); + + const chunksFinishedWriting = []; + const writableStartPromise = Promise.resolve(); + let writeCalled = false; + const ws = recordingWritableStream({ + start() { + return writableStartPromise; + }, + write(chunk) { + const waitForStep = writeCalled ? 12 : 9; + writeCalled = true; + return steps.waitThenAdvance(waitForStep).then(() => { + chunksFinishedWriting.push(chunk); + }); + } + }); + + return writableStartPromise.then(() => { + const pipePromise = rs.pipeTo(ws); + steps.waitThenAdvance(0); + + return Promise.all([ + steps.waitThenAdvance(2).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 2, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 2, one chunk must have been written'); + + // When 'a' (the very first chunk) was enqueued, it was immediately used to fulfill the outstanding read request + // promise, leaving the queue empty. + assert_array_equals(desiredSizes, [1], + 'at step 2, the desiredSize at the last enqueue (step 1) must have been 1'); + assert_equals(rs.controller.desiredSize, 1, 'at step 2, the current desiredSize must be 1'); + }), + + steps.waitThenAdvance(4).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 4, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 4, one chunk must have been written'); + + // When 'b' was enqueued at step 3, the queue was also empty, since immediately after enqueuing 'a' at + // step 1, it was dequeued in order to fulfill the read() call that was made at step 0. Thus the queue + // had size 1 (thus desiredSize of 0). + assert_array_equals(desiredSizes, [1, 0], + 'at step 4, the desiredSize at the last enqueue (step 3) must have been 0'); + assert_equals(rs.controller.desiredSize, 0, 'at step 4, the current desiredSize must be 0'); + }), + + steps.waitThenAdvance(6).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 6, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 6, one chunk must have been written'); + + // When 'c' was enqueued at step 5, the queue was not empty; it had 'b' in it, since 'b' will not be read until + // the first write completes at step 9. Thus, the queue size is 2 after enqueuing 'c', giving a desiredSize of + // -1. + assert_array_equals(desiredSizes, [1, 0, -1], + 'at step 6, the desiredSize at the last enqueue (step 5) must have been -1'); + assert_equals(rs.controller.desiredSize, -1, 'at step 6, the current desiredSize must be -1'); + }), + + steps.waitThenAdvance(8).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 8, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 8, one chunk must have been written'); + + // When 'd' was enqueued at step 7, the situation is the same as before, leading to a queue containing 'b', 'c', + // and 'd'. + assert_array_equals(desiredSizes, [1, 0, -1, -2], + 'at step 8, the desiredSize at the last enqueue (step 7) must have been -2'); + assert_equals(rs.controller.desiredSize, -2, 'at step 8, the current desiredSize must be -2'); + }), + + steps.waitThenAdvance(10).then(() => { + assert_array_equals(chunksFinishedWriting, ['a'], 'at step 10, one chunk must have finished writing'); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'at step 10, two chunks must have been written'); + + assert_equals(rs.controller.desiredSize, -1, 'at step 10, the current desiredSize must be -1'); + }), + + pipePromise.then(() => { + assert_array_equals(desiredSizes, [1, 0, -1, -2], 'backpressure must have been exerted at the source'); + assert_array_equals(chunksFinishedWriting, ['a', 'b', 'c', 'd'], 'all chunks finished writing'); + + assert_array_equals(rs.eventsWithoutPulls, [], 'nothing unexpected should happen to the ReadableStream'); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c', 'write', 'd', 'close'], + 'all chunks were written (and the WritableStream closed)'); + }) + ]); + }); +}, 'Piping to a WritableStream that does not consume the writes fast enough exerts backpressure on the ReadableStream'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html new file mode 100644 index 00000000000..7bdc8bf6ad8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js new file mode 100644 index 00000000000..f051d8102c2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js @@ -0,0 +1,212 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/recording-streams.js +'use strict'; + +test(() => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + assert_false(rs.locked, 'sanity check: the ReadableStream must not start locked'); + assert_false(ws.locked, 'sanity check: the WritableStream must not start locked'); + + rs.pipeTo(ws); + + assert_true(rs.locked, 'the ReadableStream must become locked'); + assert_true(ws.locked, 'the WritableStream must become locked'); + +}, 'Piping must lock both the ReadableStream and WritableStream'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + + return rs.pipeTo(ws).then(() => { + assert_false(rs.locked, 'the ReadableStream must become unlocked'); + assert_false(ws.locked, 'the WritableStream must become unlocked'); + }); + +}, 'Piping finishing must unlock both the ReadableStream and WritableStream'); + +promise_test(t => { + + const fakeRS = Object.create(ReadableStream.prototype); + const ws = new WritableStream(); + + return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(fakeRS, [ws]), + 'pipeTo should reject with a TypeError'); + +}, 'pipeTo must check the brand of its ReadableStream this value'); + +promise_test(t => { + + const rs = new ReadableStream(); + const fakeWS = Object.create(WritableStream.prototype); + + return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(rs, [fakeWS]), + 'pipeTo should reject with a TypeError'); + +}, 'pipeTo must check the brand of its WritableStream argument'); + +promise_test(t => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + rs.getReader(); + + assert_true(rs.locked, 'sanity check: the ReadableStream starts locked'); + assert_false(ws.locked, 'sanity check: the WritableStream does not start locked'); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws)).then(() => { + assert_false(ws.locked, 'the WritableStream must still be unlocked'); + }); + +}, 'pipeTo must fail if the ReadableStream is locked, and not lock the WritableStream'); + +promise_test(t => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + ws.getWriter(); + + assert_false(rs.locked, 'sanity check: the ReadableStream does not start locked'); + assert_true(ws.locked, 'sanity check: the WritableStream starts locked'); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws)).then(() => { + assert_false(rs.locked, 'the ReadableStream must still be unlocked'); + }); + +}, 'pipeTo must fail if the WritableStream is locked, and not lock the ReadableStream'); + +promise_test(() => { + + const CHUNKS = 10; + + const rs = new ReadableStream({ + start(c) { + for (let i = 0; i < CHUNKS; ++i) { + c.enqueue(i); + } + c.close(); + } + }); + + const written = []; + const ws = new WritableStream({ + write(chunk) { + written.push(chunk); + }, + close() { + written.push('closed'); + } + }, new CountQueuingStrategy({ highWaterMark: CHUNKS })); + + return rs.pipeTo(ws).then(() => { + const targetValues = []; + for (let i = 0; i < CHUNKS; ++i) { + targetValues.push(i); + } + targetValues.push('closed'); + + assert_array_equals(written, targetValues, 'the correct values must be written'); + + // Ensure both readable and writable are closed by the time the pipe finishes. + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + + // NOTE: no requirement on *when* the pipe finishes; that is left to implementations. + +}, 'Piping from a ReadableStream from which lots of chunks are synchronously readable'); + +promise_test(t => { + + let controller; + const rs = recordingReadableStream({ + start(c) { + controller = c; + } + }); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws).then(() => { + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + }); + + t.step_timeout(() => { + controller.enqueue('Hello'); + t.step_timeout(() => controller.close(), 10); + }, 10); + + return pipePromise; + +}, 'Piping from a ReadableStream for which a chunk becomes asynchronously readable after the pipeTo'); + +for (const preventAbort of [true, false]) { + promise_test(() => { + + const rs = new ReadableStream({ + pull() { + return Promise.reject(undefined); + } + }); + + return rs.pipeTo(new WritableStream(), { preventAbort }).then( + () => assert_unreached('pipeTo promise should be rejected'), + value => assert_equals(value, undefined, 'rejection value should be undefined')); + + }, `an undefined rejection from pull should cause pipeTo() to reject when preventAbort is ${preventAbort}`); +} + +for (const preventCancel of [true, false]) { + promise_test(() => { + + const rs = new ReadableStream({ + pull(controller) { + controller.enqueue(0); + } + }); + + const ws = new WritableStream({ + write() { + return Promise.reject(undefined); + } + }); + + return rs.pipeTo(ws, { preventCancel }).then( + () => assert_unreached('pipeTo promise should be rejected'), + value => assert_equals(value, undefined, 'rejection value should be undefined')); + + }, `an undefined rejection from write should cause pipeTo() to reject when preventCancel is ${preventCancel}`); +} + +promise_test(t => { + const rs = new ReadableStream(); + const ws = new WritableStream(); + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { + get preventAbort() { + ws.getWriter(); + } + }), 'pipeTo should reject'); +}, 'pipeTo() should reject if an option getter grabs a writer'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + + return rs.pipeTo(ws, null); +}, 'pipeTo() promise should resolve if null is passed'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html new file mode 100644 index 00000000000..b71f29922b3 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js new file mode 100644 index 00000000000..9be828a2326 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js @@ -0,0 +1,227 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +function createErroredWritableStream(t) { + return Promise.resolve().then(() => { + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + const writer = ws.getWriter(); + return promise_rejects_exactly(t, error2, writer.closed, 'the writable stream must be errored with error2') + .then(() => { + writer.releaseLock(); + assert_array_equals(ws.events, []); + return ws; + }); + }); +} + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + // Trying to abort a stream that is erroring will give the writable's error + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error2, ws.getWriter().closed, 'the writable stream must be errored with error2') + ]); + }); + +}, 'Piping from an errored readable stream to an erroring writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'); + }); +}, 'Piping from an errored readable stream to an errored writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the readable stream\'s error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error2, ws.getWriter().closed, 'the writable stream must be errored with error2') + ]); + }); + +}, 'Piping from an errored readable stream to an erroring writable stream; preventAbort = true'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the readable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'); + }); + +}, 'Piping from an errored readable stream to an errored writable stream; preventAbort = true'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error1, ws.getWriter().closed, + 'closed must reject with error1'), + promise_rejects_exactly(t, error1, closePromise, + 'close() must reject with error1') + ]); + }); + +}, 'Piping from an errored readable stream to a closing writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + + return flushAsyncEvents().then(() => { + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + ws.getWriter().closed, + closePromise + ]); + }); + }); + +}, 'Piping from an errored readable stream to a closed writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error1); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed, 'the writable stream must be errored with error1') + ]); + }); + +}, 'Piping from a closed readable stream to an erroring writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return rs.getReader().closed; + }); + +}, 'Piping from a closed readable stream to an errored writable stream'); + +promise_test(() => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws).then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Piping from a closed readable stream to a closed writable stream'); From 7539d808cdf97d186ac89bb966425dc1d7688c89 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 11 Apr 2025 12:57:49 -0400 Subject: [PATCH 72/83] LibWeb/WebDriver: Add a FIXME about allowing `await` in script bodies There will soon only be a couple of remaining script execution WPT promise.py failures. This comment is to explain why. --- Libraries/LibWeb/WebDriver/ExecuteScript.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp index a7db8feccc4..7a5823e2caa 100644 --- a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp +++ b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp @@ -34,6 +34,8 @@ static JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingCo auto& realm = environment_settings.realm(); auto& global_scope = realm.global_environment(); + // FIXME: This does not handle scripts which contain `await` statements. It is not as as simple as declaring this + // function async, unfortunately. See: https://github.com/w3c/webdriver/issues/1436 auto source_text = ByteString::formatted( R"~~~(function() {{ {} From 8b7bbc81e3bbd99e400b85388673f88440170f73 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 11 Apr 2025 12:13:32 -0400 Subject: [PATCH 73/83] LibWeb/WebDriver: Execute script bodies in a promise-calling manner This is a bit of a peculiarity with the synchronous script executor. We must wrap the script result in a promise. --- Libraries/LibWeb/WebDriver/ExecuteScript.cpp | 43 ++++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp index 7a5823e2caa..6af46669f7f 100644 --- a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp +++ b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp @@ -21,6 +21,19 @@ namespace Web::WebDriver { +// https://w3ctag.github.io/promises-guide/#should-promise-call +static GC::Ref promise_call(JS::Realm& realm, JS::ThrowCompletionOr result) +{ + // If the developer supplies you with a function that you expect to return a promise, you should also allow it to + // return a thenable or non-promise value, or even throw an exception, and treat all these cases as if they had + // returned an analogous promise. This should be done by converting the returned value to a promise, as if by using + // Promise.resolve(), and catching thrown exceptions and converting those into a promise as if by using + // Promise.reject(). We call this "promise-calling" the function. + if (result.is_error()) + return WebIDL::create_rejected_promise(realm, result.error_value()); + return WebIDL::create_resolved_promise(realm, result.release_value()); +} + // https://w3c.github.io/webdriver/#dfn-execute-a-function-body static JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingContext const& browsing_context, StringView body, ReadonlySpan parameters) { @@ -116,21 +129,25 @@ void execute_script(HTML::BrowsingContext const& browsing_context, String body, // 8. Run the following substeps in parallel: Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, &browsing_context, promise, body = move(body), arguments = move(arguments)]() mutable { - HTML::TemporaryExecutionContext execution_context { realm }; + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 1. Let scriptPromise be the result of promise-calling execute a function body, with arguments body and arguments. - auto script_result = execute_a_function_body(browsing_context, body, move(arguments)); + auto script_promise = promise_call(realm, execute_a_function_body(browsing_context, body, arguments)); - // FIXME: This isn't right, we should be reacting to this using WebIDL::react_to_promise() - // 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v. - if (script_result.has_value()) { - WebIDL::resolve_promise(realm, promise, script_result.release_value()); - } + WebIDL::react_to_promise(script_promise, + // 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v. + GC::create_function(realm.heap(), [&realm, promise](JS::Value value) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::resolve_promise(realm, promise, value); + return JS::js_undefined(); + }), - // 3. Upon rejection of scriptPromise with value r, reject promise with value r. - if (script_result.is_throw_completion()) { - WebIDL::reject_promise(realm, promise, script_result.throw_completion().value()); - } + // 3. Upon rejection of scriptPromise with value r, reject promise with value r. + GC::create_function(realm.heap(), [&realm, promise](JS::Value reason) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::reject_promise(realm, promise, reason); + return JS::js_undefined(); + })); })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. @@ -139,8 +156,8 @@ void execute_script(HTML::BrowsingContext const& browsing_context, String body, return JS::js_undefined(); timer->stop(); - auto promise_promise = GC::Ref { as(*promise->promise()) }; - on_complete->function()({ promise_promise->state(), promise_promise->result() }); + auto const& underlying_promise = as(*promise->promise()); + on_complete->function()({ underlying_promise.state(), underlying_promise.result() }); return JS::js_undefined(); }); From 0b23717bc97a4b12839e029911d280309217c85a Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 11 Apr 2025 12:55:24 -0400 Subject: [PATCH 74/83] LibWeb/WebDriver: Use WebIDL promise AOs to execute async scripts This removes the use of `spin_event_loop_until` when waiting for async script results. --- Libraries/LibWeb/WebDriver/ExecuteScript.cpp | 81 +++++++++----------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp index 6af46669f7f..e4951b8c10a 100644 --- a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp +++ b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp @@ -104,6 +104,22 @@ static JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingCo return completion; } +static void fire_completion_when_resolved(GC::Ref promise, GC::Ref timer, GC::Ref on_complete) +{ + auto reaction_steps = GC::create_function(promise->heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { + if (timer->is_timed_out()) + return JS::js_undefined(); + timer->stop(); + + auto const& underlying_promise = as(*promise->promise()); + on_complete->function()({ underlying_promise.state(), underlying_promise.result() }); + + return JS::js_undefined(); + }); + + WebIDL::react_to_promise(promise, reaction_steps, reaction_steps); +} + void execute_script(HTML::BrowsingContext const& browsing_context, String body, GC::RootVector arguments, Optional const& timeout_ms, GC::Ref on_complete) { auto const* document = browsing_context.active_document(); @@ -151,18 +167,7 @@ void execute_script(HTML::BrowsingContext const& browsing_context, String body, })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. - auto reaction_steps = GC::create_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { - if (timer->is_timed_out()) - return JS::js_undefined(); - timer->stop(); - - auto const& underlying_promise = as(*promise->promise()); - on_complete->function()({ underlying_promise.state(), underlying_promise.result() }); - - return JS::js_undefined(); - }); - - WebIDL::react_to_promise(promise, reaction_steps, reaction_steps); + fire_completion_when_resolved(promise, timer, on_complete); } // https://w3c.github.io/webdriver/#execute-async-script @@ -187,15 +192,14 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 7. Let promise be a new Promise. - auto promise_capability = WebIDL::create_promise(realm); - GC::Ref promise { as(*promise_capability->promise()) }; + auto promise = WebIDL::create_promise(realm); // 8. Run the following substeps in parallel: - Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&vm, &realm, &browsing_context, timer, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable { - HTML::TemporaryExecutionContext execution_context { realm }; + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&vm, &realm, &browsing_context, promise, body = move(body), arguments = move(arguments)]() mutable { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 1. Let resolvingFunctions be CreateResolvingFunctions(promise). - auto resolving_functions = promise->create_resolving_functions(); + auto resolving_functions = as(*promise->promise()).create_resolving_functions(); // 2. Append resolvingFunctions.[[Resolve]] to arguments. arguments.append(resolving_functions.resolve); @@ -208,7 +212,7 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String // In order to preserve legacy behavior, the return value only influences the command if it is a // "thenable" object or if determining this produces an exception. if (script_result.is_throw_completion()) { - promise->reject(script_result.throw_completion().value()); + WebIDL::reject_promise(realm, promise, script_result.error_value()); return; } @@ -221,7 +225,7 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String // 7. If then.[[Type]] is not normal, then reject promise with value then.[[Value]], and abort these steps. if (then.is_throw_completion()) { - promise->reject(then.throw_completion().value()); + WebIDL::reject_promise(realm, promise, then.error_value()); return; } @@ -230,35 +234,26 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String return; // 9. Let scriptPromise be PromiseResolve(Promise, scriptResult.[[Value]]). - auto script_promise_or_error = JS::promise_resolve(vm, realm.intrinsics().promise_constructor(), script_result.value()); - if (script_promise_or_error.is_throw_completion()) - return; - auto& script_promise = static_cast(*script_promise_or_error.value()); + auto script_promise = WebIDL::create_resolved_promise(realm, script_result.value()); - vm.custom_data()->spin_event_loop_until(GC::create_function(vm.heap(), [timer, &script_promise]() { - return timer->is_timed_out() || script_promise.state() != JS::Promise::State::Pending; - })); + WebIDL::react_to_promise(script_promise, + // 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v. + GC::create_function(realm.heap(), [&realm, promise](JS::Value value) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::resolve_promise(realm, promise, value); + return JS::js_undefined(); + }), - // 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v. - if (script_promise.state() == JS::Promise::State::Fulfilled) - WebIDL::resolve_promise(realm, promise_capability, script_promise.result()); - - // 11. Upon rejection of scriptPromise with value r, reject promise with value r. - if (script_promise.state() == JS::Promise::State::Rejected) - WebIDL::reject_promise(realm, promise_capability, script_promise.result()); + // 11. Upon rejection of scriptPromise with value r, reject promise with value r. + GC::create_function(realm.heap(), [&realm, promise](JS::Value reason) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::reject_promise(realm, promise, reason); + return JS::js_undefined(); + })); })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. - auto reaction_steps = GC::create_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { - if (timer->is_timed_out()) - return JS::js_undefined(); - timer->stop(); - - on_complete->function()({ promise->state(), promise->result() }); - return JS::js_undefined(); - }); - - WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps); + fire_completion_when_resolved(promise, timer, on_complete); } } From 8ec72d6906108dd3c6cda1cc669e2ca668c3adac Mon Sep 17 00:00:00 2001 From: mikiubo Date: Mon, 7 Apr 2025 23:13:31 +0200 Subject: [PATCH 75/83] LibUnicode: Avoid rejecting end-of-text position as a valid boundary When the cursor was positioned at the end of text, attempting to move it left(using the left arrow key) would fail because align_boundary() was rejecting the end-of-text position as a valid boundary. --- Libraries/LibUnicode/Segmenter.cpp | 28 ++++++++++++---------------- Tests/LibUnicode/TestSegmenter.cpp | 28 ++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Libraries/LibUnicode/Segmenter.cpp b/Libraries/LibUnicode/Segmenter.cpp index db66f9e5516..63023995d88 100644 --- a/Libraries/LibUnicode/Segmenter.cpp +++ b/Libraries/LibUnicode/Segmenter.cpp @@ -87,15 +87,13 @@ public: virtual Optional previous_boundary(size_t boundary, Inclusive inclusive) override { auto icu_boundary = align_boundary(boundary); - if (!icu_boundary.has_value()) - return {}; if (inclusive == Inclusive::Yes) { - if (static_cast(m_segmenter->isBoundary(*icu_boundary))) - return static_cast(*icu_boundary); + if (static_cast(m_segmenter->isBoundary(icu_boundary))) + return static_cast(icu_boundary); } - if (auto index = m_segmenter->preceding(*icu_boundary); index != icu::BreakIterator::DONE) + if (auto index = m_segmenter->preceding(icu_boundary); index != icu::BreakIterator::DONE) return static_cast(index); return {}; @@ -104,15 +102,13 @@ public: virtual Optional next_boundary(size_t boundary, Inclusive inclusive) override { auto icu_boundary = align_boundary(boundary); - if (!icu_boundary.has_value()) - return {}; if (inclusive == Inclusive::Yes) { - if (static_cast(m_segmenter->isBoundary(*icu_boundary))) - return static_cast(*icu_boundary); + if (static_cast(m_segmenter->isBoundary(icu_boundary))) + return static_cast(icu_boundary); } - if (auto index = m_segmenter->following(*icu_boundary); index != icu::BreakIterator::DONE) + if (auto index = m_segmenter->following(icu_boundary); index != icu::BreakIterator::DONE) return static_cast(index); return {}; @@ -177,25 +173,25 @@ public: } private: - Optional align_boundary(size_t boundary) + i32 align_boundary(size_t boundary) { auto icu_boundary = static_cast(boundary); return m_segmented_text.visit( - [&](String const& text) -> Optional { + [&](String const& text) { if (boundary >= text.byte_count()) - return {}; + return static_cast(text.byte_count()); U8_SET_CP_START(text.bytes().data(), 0, icu_boundary); return icu_boundary; }, - [&](icu::UnicodeString const& text) -> Optional { + [&](icu::UnicodeString const& text) { if (icu_boundary >= text.length()) - return {}; + return text.length(); return text.getChar32Start(icu_boundary); }, - [](Empty) -> Optional { VERIFY_NOT_REACHED(); }); + [](Empty) -> i32 { VERIFY_NOT_REACHED(); }); } void for_each_boundary(SegmentationCallback callback) diff --git a/Tests/LibUnicode/TestSegmenter.cpp b/Tests/LibUnicode/TestSegmenter.cpp index d40ea82cebf..13368ab8451 100644 --- a/Tests/LibUnicode/TestSegmenter.cpp +++ b/Tests/LibUnicode/TestSegmenter.cpp @@ -136,11 +136,23 @@ TEST_CASE(out_of_bounds) auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); segmenter->set_segmented_text(text); - auto result = segmenter->previous_boundary(text.byte_count()); + auto result = segmenter->previous_boundary(text.byte_count() + 1); + EXPECT(result.has_value()); + + result = segmenter->next_boundary(text.byte_count() + 1); EXPECT(!result.has_value()); + result = segmenter->previous_boundary(text.byte_count()); + EXPECT(result.has_value()); + result = segmenter->next_boundary(text.byte_count()); EXPECT(!result.has_value()); + + result = segmenter->next_boundary(0); + EXPECT(result.has_value()); + + result = segmenter->previous_boundary(0); + EXPECT(!result.has_value()); } { auto text = MUST(AK::utf8_to_utf16("foo"sv)); @@ -148,10 +160,22 @@ TEST_CASE(out_of_bounds) auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); segmenter->set_segmented_text(Utf16View { text }); - auto result = segmenter->previous_boundary(text.size()); + auto result = segmenter->previous_boundary(text.size() + 1); + EXPECT(result.has_value()); + + result = segmenter->next_boundary(text.size() + 1); EXPECT(!result.has_value()); + result = segmenter->previous_boundary(text.size()); + EXPECT(result.has_value()); + result = segmenter->next_boundary(text.size()); EXPECT(!result.has_value()); + + result = segmenter->next_boundary(0); + EXPECT(result.has_value()); + + result = segmenter->previous_boundary(0); + EXPECT(!result.has_value()); } } From 11b6bd8138c973160860058e03ee5656d480862c Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 11 Apr 2025 17:49:12 +0100 Subject: [PATCH 76/83] LibWeb/DOM: Stub out Element pointerevents methods --- Libraries/LibWeb/DOM/Element.idl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Libraries/LibWeb/DOM/Element.idl b/Libraries/LibWeb/DOM/Element.idl index e7df24216c4..ada8e5e357d 100644 --- a/Libraries/LibWeb/DOM/Element.idl +++ b/Libraries/LibWeb/DOM/Element.idl @@ -120,6 +120,10 @@ interface Element : Node { // FIXME: [CEReactions] undefined insertAdjacentHTML(DOMString position, (TrustedHTML or DOMString) string); [CEReactions] undefined insertAdjacentHTML(DOMString position, DOMString text); + // https://w3c.github.io/pointerevents/#extensions-to-the-element-interface + [FIXME] undefined setPointerCapture(long pointerId); + [FIXME] undefined releasePointerCapture(long pointerId); + [FIXME] boolean hasPointerCapture(long pointerId); }; dictionary GetHTMLOptions { From d855adf767d6ba594847377123b7690134404473 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 11 Apr 2025 16:04:23 +0100 Subject: [PATCH 77/83] LibWeb: Add slider- prefix to slider pseudo-element names Corresponds to https://github.com/w3c/csswg-drafts/commit/9549bb8951dc46e3a4a09b79c117966eb1687c13 --- Libraries/LibWeb/CSS/Default.css | 20 +++++------ Libraries/LibWeb/CSS/PseudoElements.json | 34 +++++++++---------- Libraries/LibWeb/HTML/HTMLInputElement.cpp | 6 ++-- Libraries/LibWeb/HTML/HTMLMeterElement.cpp | 4 +-- Libraries/LibWeb/HTML/HTMLProgressElement.cpp | 4 +-- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Libraries/LibWeb/CSS/Default.css b/Libraries/LibWeb/CSS/Default.css index 6cbcfaeb7b6..97992619541 100644 --- a/Libraries/LibWeb/CSS/Default.css +++ b/Libraries/LibWeb/CSS/Default.css @@ -79,7 +79,7 @@ input[type=range] { width: 20ch; height: 16px; - &::track { + &::slider-track { display: block; position: relative; height: 4px; @@ -89,14 +89,14 @@ input[type=range] { border: 1px solid rgba(0, 0, 0, 0.5); } - &::fill { + &::slider-fill { display: block; position: absolute; height: 100%; background-color: AccentColor; } - &::thumb { + &::slider-thumb { display: block; margin-top: -6px; width: 16px; @@ -115,27 +115,27 @@ meter { width: 300px; height: 12px; - &::track { + &::slider-track { display: block; height: 100%; background-color: hsl(0, 0%, 96%); border: 1px solid rgba(0, 0, 0, 0.5); } - &::fill { + &::slider-fill { display: block; height: 100%; } - &:optimal-value::fill { + &:optimal-value::slider-fill { background-color: hsl(141, 53%, 53%); } - &:suboptimal-value::fill { + &:suboptimal-value::slider-fill { background-color: hsl(48, 100%, 67%); } - &:even-less-good-value::fill { + &:even-less-good-value::slider-fill { background-color: hsl(348, 100%, 61%); } } @@ -146,14 +146,14 @@ progress { width: 300px; height: 12px; - &::track { + &::slider-track { display: block; height: 100%; background-color: AccentColorText; border: 1px solid rgba(0, 0, 0, 0.5); } - &::fill { + &::slider-fill { display: block; height: 100%; background-color: AccentColor; diff --git a/Libraries/LibWeb/CSS/PseudoElements.json b/Libraries/LibWeb/CSS/PseudoElements.json index 65c93fa6e6d..1e702281795 100644 --- a/Libraries/LibWeb/CSS/PseudoElements.json +++ b/Libraries/LibWeb/CSS/PseudoElements.json @@ -1,33 +1,33 @@ { "-moz-meter-bar": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-moz-progress-bar": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-moz-range-progress": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-moz-range-track": { - "alias-for": "track" + "alias-for": "slider-track" }, "-moz-range-thumb": { - "alias-for": "thumb" + "alias-for": "slider-thumb" }, "-webkit-meter-bar": { - "alias-for": "track" + "alias-for": "slider-track" }, "-webkit-progress-bar": { - "alias-for": "track" + "alias-for": "slider-track" }, "-webkit-progress-value": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-webkit-slider-runnable-track": { - "alias-for": "track" + "alias-for": "slider-track" }, "-webkit-slider-thumb": { - "alias-for": "thumb" + "alias-for": "slider-thumb" }, "after": { "spec": "https://drafts.csswg.org/css-pseudo-4/#selectordef-after", @@ -47,9 +47,6 @@ "file-selector-button": { "spec": "https://drafts.csswg.org/css-pseudo-4/#selectordef-file-selector-button" }, - "fill": { - "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-fill" - }, "first-letter": { "spec": "https://drafts.csswg.org/css-pseudo-4/#selectordef-first-letter", "property-whitelist": [ @@ -107,11 +104,14 @@ "#custom-properties" ] }, - "thumb": { - "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-thumb" + "slider-fill": { + "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-fill" }, - "track": { - "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-track" + "slider-thumb": { + "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-thumb" + }, + "slider-track": { + "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-track" }, "view-transition": { "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition" diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 7a63fa03c45..8818ec4f76a 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -1217,15 +1217,15 @@ void HTMLInputElement::create_range_input_shadow_tree() set_shadow_root(shadow_root); m_slider_runnable_track = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_slider_runnable_track->set_use_pseudo_element(CSS::PseudoElement::Track); + m_slider_runnable_track->set_use_pseudo_element(CSS::PseudoElement::SliderTrack); MUST(shadow_root->append_child(*m_slider_runnable_track)); m_slider_progress_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_slider_progress_element->set_use_pseudo_element(CSS::PseudoElement::Fill); + m_slider_progress_element->set_use_pseudo_element(CSS::PseudoElement::SliderFill); MUST(m_slider_runnable_track->append_child(*m_slider_progress_element)); m_slider_thumb = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_slider_thumb->set_use_pseudo_element(CSS::PseudoElement::Thumb); + m_slider_thumb->set_use_pseudo_element(CSS::PseudoElement::SliderThumb); MUST(m_slider_runnable_track->append_child(*m_slider_thumb)); update_slider_shadow_tree_elements(); diff --git a/Libraries/LibWeb/HTML/HTMLMeterElement.cpp b/Libraries/LibWeb/HTML/HTMLMeterElement.cpp index e1111ba878e..87bcd2a7cdb 100644 --- a/Libraries/LibWeb/HTML/HTMLMeterElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLMeterElement.cpp @@ -197,11 +197,11 @@ void HTMLMeterElement::create_shadow_tree_if_needed() set_shadow_root(shadow_root); auto meter_bar_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - meter_bar_element->set_use_pseudo_element(CSS::PseudoElement::Track); + meter_bar_element->set_use_pseudo_element(CSS::PseudoElement::SliderTrack); MUST(shadow_root->append_child(*meter_bar_element)); m_meter_value_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_meter_value_element->set_use_pseudo_element(CSS::PseudoElement::Fill); + m_meter_value_element->set_use_pseudo_element(CSS::PseudoElement::SliderFill); MUST(meter_bar_element->append_child(*m_meter_value_element)); update_meter_value_element(); } diff --git a/Libraries/LibWeb/HTML/HTMLProgressElement.cpp b/Libraries/LibWeb/HTML/HTMLProgressElement.cpp index 325dd034b48..505ec74128b 100644 --- a/Libraries/LibWeb/HTML/HTMLProgressElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLProgressElement.cpp @@ -118,11 +118,11 @@ void HTMLProgressElement::create_shadow_tree_if_needed() set_shadow_root(shadow_root); auto progress_bar_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - progress_bar_element->set_use_pseudo_element(CSS::PseudoElement::Track); + progress_bar_element->set_use_pseudo_element(CSS::PseudoElement::SliderTrack); MUST(shadow_root->append_child(*progress_bar_element)); m_progress_value_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_progress_value_element->set_use_pseudo_element(CSS::PseudoElement::Fill); + m_progress_value_element->set_use_pseudo_element(CSS::PseudoElement::SliderFill); MUST(progress_bar_element->append_child(*m_progress_value_element)); update_progress_value_element(); } From 6507d23e2908156fc53104c1fbeb4609cd38db71 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Fri, 11 Apr 2025 20:00:56 +0200 Subject: [PATCH 78/83] LibWeb: Save ScrollState snapshot in DisplayList to avoid race condition With this change we save a copy of of scroll state at the time of recording a display list, instead of actual ScrollState pointer that could be modifed by the main thread while display list is beings rasterized on the rendering thread, which leads to a frame painted with inconsistent scroll state. Fixes https://github.com/LadybirdBrowser/ladybird/issues/4288 --- Libraries/LibWeb/CMakeLists.txt | 1 + .../CSS/StyleValues/CursorStyleValue.cpp | 3 +- Libraries/LibWeb/DOM/Document.cpp | 4 +-- Libraries/LibWeb/HTML/RenderingThread.cpp | 6 ++-- Libraries/LibWeb/HTML/RenderingThread.h | 3 +- .../LibWeb/HTML/TraversableNavigable.cpp | 3 +- Libraries/LibWeb/Painting/Command.h | 4 +-- Libraries/LibWeb/Painting/DisplayList.cpp | 7 ++-- Libraries/LibWeb/Painting/DisplayList.h | 8 ++--- .../LibWeb/Painting/DisplayListPlayerSkia.cpp | 5 +-- .../LibWeb/Painting/DisplayListRecorder.cpp | 4 +-- .../LibWeb/Painting/DisplayListRecorder.h | 2 +- .../NavigableContainerViewportPaintable.cpp | 3 +- Libraries/LibWeb/Painting/SVGMaskable.cpp | 3 +- Libraries/LibWeb/Painting/ScrollFrame.h | 1 + Libraries/LibWeb/Painting/ScrollState.cpp | 20 +++++++++++ Libraries/LibWeb/Painting/ScrollState.h | 34 ++++++++++++++++++- Libraries/LibWeb/SVG/SVGDecodedImageData.cpp | 3 +- 18 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 Libraries/LibWeb/Painting/ScrollState.cpp diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index b9f3eab6734..94af3e8fd4a 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -691,6 +691,7 @@ set(SOURCES Painting/SVGPaintable.cpp Painting/SVGSVGPaintable.cpp Painting/ScrollFrame.cpp + Painting/ScrollState.cpp Painting/ShadowPainting.cpp Painting/StackingContext.cpp Painting/TableBordersPainting.cpp diff --git a/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp index 439921dd79c..21ab3348a19 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp @@ -94,7 +94,8 @@ Optional CursorStyleValue::make_image_cursor(Layout::NodeWithS case DisplayListPlayerType::SkiaCPU: { auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(bitmap); Painting::DisplayListPlayerSkia display_list_player; - display_list_player.execute(*display_list, painting_surface); + Painting::ScrollStateSnapshot scroll_state_snapshot; + display_list_player.execute(*display_list, scroll_state_snapshot, painting_surface); break; } } diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index a159978c1f7..64ff0637b70 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -6296,8 +6296,9 @@ void Document::invalidate_display_list() RefPtr Document::record_display_list(PaintConfig config) { - if (m_cached_display_list && m_cached_display_list_paint_config == config) + if (m_cached_display_list && m_cached_display_list_paint_config == config) { return m_cached_display_list; + } auto display_list = Painting::DisplayList::create(); Painting::DisplayListRecorder display_list_recorder(display_list); @@ -6354,7 +6355,6 @@ RefPtr Document::record_display_list(PaintConfig config) viewport_paintable.paint_all_phases(context); display_list->set_device_pixels_per_css_pixel(page().client().device_pixels_per_css_pixel()); - display_list->set_scroll_state(viewport_paintable.scroll_state()); m_cached_display_list = display_list; m_cached_display_list_paint_config = config; diff --git a/Libraries/LibWeb/HTML/RenderingThread.cpp b/Libraries/LibWeb/HTML/RenderingThread.cpp index 41effe7809b..89432dc462b 100644 --- a/Libraries/LibWeb/HTML/RenderingThread.cpp +++ b/Libraries/LibWeb/HTML/RenderingThread.cpp @@ -57,17 +57,17 @@ void RenderingThread::rendering_thread_loop() } auto painting_surface = painting_surface_for_backing_store(task->backing_store); - m_skia_player->execute(*task->display_list, painting_surface); + m_skia_player->execute(*task->display_list, task->scroll_state_snapshot, painting_surface); m_main_thread_event_loop.deferred_invoke([callback = move(task->callback)] { callback(); }); } } -void RenderingThread::enqueue_rendering_task(NonnullRefPtr display_list, NonnullRefPtr backing_store, Function&& callback) +void RenderingThread::enqueue_rendering_task(NonnullRefPtr display_list, Painting::ScrollStateSnapshot&& scroll_state_snapshot, NonnullRefPtr backing_store, Function&& callback) { Threading::MutexLocker const locker { m_rendering_task_mutex }; - m_rendering_tasks.enqueue(Task { move(display_list), move(backing_store), move(callback) }); + m_rendering_tasks.enqueue(Task { move(display_list), move(scroll_state_snapshot), move(backing_store), move(callback) }); m_rendering_task_ready_wake_condition.signal(); } diff --git a/Libraries/LibWeb/HTML/RenderingThread.h b/Libraries/LibWeb/HTML/RenderingThread.h index 2aecc153093..f5e0373d68f 100644 --- a/Libraries/LibWeb/HTML/RenderingThread.h +++ b/Libraries/LibWeb/HTML/RenderingThread.h @@ -28,7 +28,7 @@ public: void start(DisplayListPlayerType); void set_skia_player(OwnPtr&& player) { m_skia_player = move(player); } void set_skia_backend_context(RefPtr context) { m_skia_backend_context = move(context); } - void enqueue_rendering_task(NonnullRefPtr, NonnullRefPtr, Function&& callback); + void enqueue_rendering_task(NonnullRefPtr, Painting::ScrollStateSnapshot&&, NonnullRefPtr, Function&& callback); void clear_bitmap_to_surface_cache(); private: @@ -46,6 +46,7 @@ private: struct Task { NonnullRefPtr display_list; + Painting::ScrollStateSnapshot scroll_state_snapshot; NonnullRefPtr backing_store; Function callback; }; diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.cpp b/Libraries/LibWeb/HTML/TraversableNavigable.cpp index ecae592a888..886858a7896 100644 --- a/Libraries/LibWeb/HTML/TraversableNavigable.cpp +++ b/Libraries/LibWeb/HTML/TraversableNavigable.cpp @@ -1426,7 +1426,8 @@ RefPtr TraversableNavigable::record_display_list(DevicePi void TraversableNavigable::start_display_list_rendering(NonnullRefPtr display_list, NonnullRefPtr backing_store, Function&& callback) { - m_rendering_thread.enqueue_rendering_task(move(display_list), move(backing_store), move(callback)); + auto scroll_state_snapshot = active_document()->paintable()->scroll_state().snapshot(); + m_rendering_thread.enqueue_rendering_task(move(display_list), move(scroll_state_snapshot), move(backing_store), move(callback)); } } diff --git a/Libraries/LibWeb/Painting/Command.h b/Libraries/LibWeb/Painting/Command.h index ac0fd887d6d..f8b3dbb4d37 100644 --- a/Libraries/LibWeb/Painting/Command.h +++ b/Libraries/LibWeb/Painting/Command.h @@ -8,13 +8,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -34,6 +32,7 @@ #include #include #include +#include namespace Web::Painting { @@ -392,6 +391,7 @@ struct AddMask { struct PaintNestedDisplayList { RefPtr display_list; + ScrollStateSnapshot scroll_state_snapshot; Gfx::IntRect rect; [[nodiscard]] Gfx::IntRect bounding_rect() const { return rect; } diff --git a/Libraries/LibWeb/Painting/DisplayList.cpp b/Libraries/LibWeb/Painting/DisplayList.cpp index 0494a10fe7d..36ef0073c2f 100644 --- a/Libraries/LibWeb/Painting/DisplayList.cpp +++ b/Libraries/LibWeb/Painting/DisplayList.cpp @@ -35,18 +35,18 @@ static bool command_is_clip_or_mask(Command const& command) }); } -void DisplayListPlayer::execute(DisplayList& display_list, RefPtr surface) +void DisplayListPlayer::execute(DisplayList& display_list, ScrollStateSnapshot const& scroll_state, RefPtr surface) { if (surface) { surface->lock_context(); } - execute_impl(display_list, surface); + execute_impl(display_list, scroll_state, surface); if (surface) { surface->unlock_context(); } } -void DisplayListPlayer::execute_impl(DisplayList& display_list, RefPtr surface) +void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnapshot const& scroll_state, RefPtr surface) { if (surface) m_surfaces.append(*surface); @@ -56,7 +56,6 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, RefPtr); + void execute(DisplayList&, ScrollStateSnapshot const&, RefPtr); protected: Gfx::PaintingSurface& surface() const { return m_surfaces.last(); } - void execute_impl(DisplayList&, RefPtr); + void execute_impl(DisplayList&, ScrollStateSnapshot const& scroll_state, RefPtr); private: virtual void flush() = 0; @@ -93,9 +93,6 @@ public: AK::SegmentedVector const& commands() const { return m_commands; } - void set_scroll_state(ScrollState scroll_state) { m_scroll_state = move(scroll_state); } - ScrollState const& scroll_state() const { return m_scroll_state; } - void set_device_pixels_per_css_pixel(double device_pixels_per_css_pixel) { m_device_pixels_per_css_pixel = device_pixels_per_css_pixel; } double device_pixels_per_css_pixel() const { return m_device_pixels_per_css_pixel; } @@ -103,7 +100,6 @@ private: DisplayList() = default; AK::SegmentedVector m_commands; - ScrollState m_scroll_state; double m_device_pixels_per_css_pixel; }; diff --git a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp index 89e96654024..c440c34a9d4 100644 --- a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp +++ b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp @@ -972,7 +972,8 @@ void DisplayListPlayerSkia::add_mask(AddMask const& command) auto mask_surface = Gfx::PaintingSurface::create_with_size(m_context, rect.size(), Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied); - execute_impl(*command.display_list, mask_surface); + ScrollStateSnapshot scroll_state_snapshot; + execute_impl(*command.display_list, scroll_state_snapshot, mask_surface); SkMatrix mask_matrix; mask_matrix.setTranslate(rect.x(), rect.y()); @@ -985,7 +986,7 @@ void DisplayListPlayerSkia::paint_nested_display_list(PaintNestedDisplayList con { auto& canvas = surface().canvas(); canvas.translate(command.rect.x(), command.rect.y()); - execute_impl(*command.display_list, {}); + execute_impl(*command.display_list, command.scroll_state_snapshot, {}); } void DisplayListPlayerSkia::paint_scrollbar(PaintScrollBar const& command) diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index 8e4c219c919..e00e7ac04f4 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp @@ -24,9 +24,9 @@ void DisplayListRecorder::append(Command&& command) m_command_list.append(move(command), scroll_frame_id); } -void DisplayListRecorder::paint_nested_display_list(RefPtr display_list, Gfx::IntRect rect) +void DisplayListRecorder::paint_nested_display_list(RefPtr display_list, ScrollStateSnapshot&& scroll_state_snapshot, Gfx::IntRect rect) { - append(PaintNestedDisplayList { move(display_list), rect }); + append(PaintNestedDisplayList { move(display_list), move(scroll_state_snapshot), rect }); } void DisplayListRecorder::add_rounded_rect_clip(CornerRadii corner_radii, Gfx::IntRect border_rect, CornerClip corner_clip) diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.h b/Libraries/LibWeb/Painting/DisplayListRecorder.h index 0da68e1ea24..86a0c022379 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.h +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.h @@ -129,7 +129,7 @@ public: void push_stacking_context(PushStackingContextParams params); void pop_stacking_context(); - void paint_nested_display_list(RefPtr display_list, Gfx::IntRect rect); + void paint_nested_display_list(RefPtr display_list, ScrollStateSnapshot&&, Gfx::IntRect rect); void add_rounded_rect_clip(CornerRadii corner_radii, Gfx::IntRect border_rect, CornerClip corner_clip); void add_mask(RefPtr display_list, Gfx::IntRect rect); diff --git a/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp b/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp index 097334e6147..bd0c675f66c 100644 --- a/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp +++ b/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp @@ -60,7 +60,8 @@ void NavigableContainerViewportPaintable::paint(PaintContext& context, PaintPhas paint_config.should_show_line_box_borders = context.should_show_line_box_borders(); paint_config.has_focus = context.has_focus(); auto display_list = const_cast(hosted_document)->record_display_list(paint_config); - context.display_list_recorder().paint_nested_display_list(display_list, context.enclosing_device_rect(absolute_rect).to_type()); + auto scroll_state_snapshot = hosted_document->paintable()->scroll_state().snapshot(); + context.display_list_recorder().paint_nested_display_list(display_list, move(scroll_state_snapshot), context.enclosing_device_rect(absolute_rect).to_type()); context.display_list_recorder().restore(); diff --git a/Libraries/LibWeb/Painting/SVGMaskable.cpp b/Libraries/LibWeb/Painting/SVGMaskable.cpp index 7185c380587..4d9e066bed1 100644 --- a/Libraries/LibWeb/Painting/SVGMaskable.cpp +++ b/Libraries/LibWeb/Painting/SVGMaskable.cpp @@ -100,7 +100,8 @@ RefPtr SVGMaskable::calculate_mask_of_svg(PaintContext& co StackingContext::paint_svg(paint_context, paintable, PaintPhase::Foreground); auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(*mask_bitmap); DisplayListPlayerSkia display_list_player; - display_list_player.execute(display_list, painting_surface); + ScrollStateSnapshot scroll_state_snapshot; + display_list_player.execute(display_list, scroll_state_snapshot, painting_surface); return mask_bitmap; }; RefPtr mask_bitmap = {}; diff --git a/Libraries/LibWeb/Painting/ScrollFrame.h b/Libraries/LibWeb/Painting/ScrollFrame.h index f5c0c9ac408..c017173d086 100644 --- a/Libraries/LibWeb/Painting/ScrollFrame.h +++ b/Libraries/LibWeb/Painting/ScrollFrame.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include diff --git a/Libraries/LibWeb/Painting/ScrollState.cpp b/Libraries/LibWeb/Painting/ScrollState.cpp new file mode 100644 index 00000000000..c817b325dff --- /dev/null +++ b/Libraries/LibWeb/Painting/ScrollState.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Web::Painting { + +ScrollStateSnapshot ScrollStateSnapshot::create(Vector> const& scroll_frames) +{ + ScrollStateSnapshot snapshot; + snapshot.entries.ensure_capacity(scroll_frames.size()); + for (auto const& scroll_frame : scroll_frames) + snapshot.entries.append({ scroll_frame->cumulative_offset(), scroll_frame->own_offset() }); + return snapshot; +} + +} diff --git a/Libraries/LibWeb/Painting/ScrollState.h b/Libraries/LibWeb/Painting/ScrollState.h index ddadbc550a0..dde14bf1ea4 100644 --- a/Libraries/LibWeb/Painting/ScrollState.h +++ b/Libraries/LibWeb/Painting/ScrollState.h @@ -1,15 +1,42 @@ /* - * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2024-2025, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include #include namespace Web::Painting { +class ScrollStateSnapshot { +public: + static ScrollStateSnapshot create(Vector> const& scroll_frames); + + CSSPixelPoint cumulative_offset_for_frame_with_id(size_t id) const + { + if (id >= entries.size()) + return {}; + return entries[id].cumulative_offset; + } + + CSSPixelPoint own_offset_for_frame_with_id(size_t id) const + { + if (id >= entries.size()) + return {}; + return entries[id].own_offset; + } + +private: + struct Entry { + CSSPixelPoint cumulative_offset; + CSSPixelPoint own_offset; + }; + Vector entries; +}; + class ScrollState { public: NonnullRefPtr create_scroll_frame_for(PaintableBox const& paintable_box, RefPtr parent) @@ -56,6 +83,11 @@ public: } } + ScrollStateSnapshot snapshot() const + { + return ScrollStateSnapshot::create(m_scroll_frames); + } + private: Vector> m_scroll_frames; }; diff --git a/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp b/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp index a7d6fba1cb3..60158f38bac 100644 --- a/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp +++ b/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp @@ -106,7 +106,8 @@ RefPtr SVGDecodedImageData::render(Gfx::IntSize size) const case DisplayListPlayerType::SkiaCPU: { auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(*bitmap); Painting::DisplayListPlayerSkia display_list_player; - display_list_player.execute(*display_list, painting_surface); + Painting::ScrollStateSnapshot scroll_state_snapshot; + display_list_player.execute(*display_list, scroll_state_snapshot, painting_surface); break; } default: From e80d1c1a86bd91a8de03db03699f4b2c0c4446f6 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 10 Apr 2025 11:37:16 +0200 Subject: [PATCH 79/83] LibJS: Add fast_is for JS::Array (array exotic objects) Nukes a 0.3% profile item on Speedometer 2.1. --- Libraries/LibJS/Runtime/Array.h | 5 +++++ Libraries/LibJS/Runtime/Object.h | 1 + 2 files changed, 6 insertions(+) diff --git a/Libraries/LibJS/Runtime/Array.h b/Libraries/LibJS/Runtime/Array.h index 6ae77de113b..e30df2335e5 100644 --- a/Libraries/LibJS/Runtime/Array.h +++ b/Libraries/LibJS/Runtime/Array.h @@ -58,11 +58,16 @@ protected: explicit Array(Object& prototype); private: + virtual bool is_array_exotic_object() const final { return true; } + ThrowCompletionOr set_length(PropertyDescriptor const&); bool m_length_writable { true }; }; +template<> +inline bool Object::fast_is() const { return is_array_exotic_object(); } + enum class Holes { SkipHoles, ReadThroughHoles, diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index 34fce27b8f0..0225c30d7cf 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -198,6 +198,7 @@ public: virtual bool is_regexp_object() const { return false; } virtual bool is_bigint_object() const { return false; } virtual bool is_string_object() const { return false; } + virtual bool is_array_exotic_object() const { return false; } virtual bool is_global_object() const { return false; } virtual bool is_proxy_object() const { return false; } virtual bool is_native_function() const { return false; } From d78e3590d5d0ca02c8a005512ff839a120731521 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 10 Apr 2025 12:18:08 +0200 Subject: [PATCH 80/83] LibJS: Don't convert to UTF-8 in order to compare two UTF-16 strings If we have two PrimitiveString objects that are both backed by UTF-16 data, we don't have to convert them to UTF-8 for equality checking. Just compare the underlying UTF-16 data. :^) --- Libraries/LibJS/Runtime/PrimitiveString.cpp | 11 +++++++++++ Libraries/LibJS/Runtime/PrimitiveString.h | 2 ++ Libraries/LibJS/Runtime/Value.cpp | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Libraries/LibJS/Runtime/PrimitiveString.cpp b/Libraries/LibJS/Runtime/PrimitiveString.cpp index a161042e2e5..1c6d8d5a56e 100644 --- a/Libraries/LibJS/Runtime/PrimitiveString.cpp +++ b/Libraries/LibJS/Runtime/PrimitiveString.cpp @@ -106,6 +106,17 @@ Utf16View PrimitiveString::utf16_string_view() const return m_utf16_string->view(); } +bool PrimitiveString::operator==(PrimitiveString const& other) const +{ + if (this == &other) + return true; + if (m_utf8_string.has_value() && other.m_utf8_string.has_value()) + return m_utf8_string->bytes_as_string_view() == other.m_utf8_string->bytes_as_string_view(); + if (m_utf16_string.has_value() && other.m_utf16_string.has_value()) + return m_utf16_string->string() == other.m_utf16_string->string(); + return utf8_string_view() == other.utf8_string_view(); +} + ThrowCompletionOr> PrimitiveString::get(VM& vm, PropertyKey const& property_key) const { if (property_key.is_symbol()) diff --git a/Libraries/LibJS/Runtime/PrimitiveString.h b/Libraries/LibJS/Runtime/PrimitiveString.h index 4fefbd785c2..7be62544c5e 100644 --- a/Libraries/LibJS/Runtime/PrimitiveString.h +++ b/Libraries/LibJS/Runtime/PrimitiveString.h @@ -47,6 +47,8 @@ public: ThrowCompletionOr> get(VM&, PropertyKey const&) const; + [[nodiscard]] bool operator==(PrimitiveString const&) const; + protected: enum class RopeTag { Rope }; explicit PrimitiveString(RopeTag) diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index a61ef9c9bef..eedf42c81f3 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -2225,7 +2225,7 @@ bool same_value_non_number(Value lhs, Value rhs) // 5. If x is a String, then if (lhs.is_string()) { // a. If x and y are exactly the same sequence of code units (same length and same code units at corresponding indices), return true; otherwise, return false. - return lhs.as_string().utf8_string_view() == rhs.as_string().utf8_string_view(); + return lhs.as_string() == rhs.as_string(); } // 3. If x is undefined, return true. From e4941a36b0f496e0be6e8594b86b1c81eca74c06 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 10 Apr 2025 12:21:22 +0200 Subject: [PATCH 81/83] LibJS: Remove unused struct NativeStackFrame --- Libraries/LibJS/Runtime/VM.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Libraries/LibJS/Runtime/VM.cpp b/Libraries/LibJS/Runtime/VM.cpp index 53e11d7d5b9..2bf87066011 100644 --- a/Libraries/LibJS/Runtime/VM.cpp +++ b/Libraries/LibJS/Runtime/VM.cpp @@ -759,13 +759,6 @@ void VM::pop_execution_context() on_call_stack_emptied(); } -#if ARCH(X86_64) -struct [[gnu::packed]] NativeStackFrame { - NativeStackFrame* prev; - FlatPtr return_address; -}; -#endif - static RefPtr get_source_range(ExecutionContext const* context) { // native function From 6db20a9453ccc4ca2a88a4abbef329b10129831f Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 11 Apr 2025 18:55:01 +0200 Subject: [PATCH 82/83] LibJS: Simplify ECMAScriptFunctionObject.[[Realm]] slot handling Our engine already keeps track of the home realm for all objects. This is stored in Shape::realm(). We can use that instead of having a dedicated member in ESFO for the same pointer. Since there's always a home realm these days, we can also remove some outdated fallback code from the days when having a realm was not guaranteed due to LibWeb shenanigans. --- .../Runtime/ECMAScriptFunctionObject.cpp | 30 ++----------------- .../LibJS/Runtime/ECMAScriptFunctionObject.h | 3 +- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index c35bdb5097f..4acb9be3b9d 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -426,10 +426,9 @@ ECMAScriptFunctionObject::ECMAScriptFunctionObject( , m_shared_data(move(shared_data)) , m_environment(parent_environment) , m_private_environment(private_environment) - , m_realm(&prototype.shape().realm()) { if (!is_arrow_function() && kind() == FunctionKind::Normal) - unsafe_set_shape(m_realm->intrinsics().normal_function_shape()); + unsafe_set_shape(realm()->intrinsics().normal_function_shape()); // 15. Set F.[[ScriptOrModule]] to GetActiveScriptOrModule(). m_script_or_module = vm().get_active_script_or_module(); @@ -643,7 +642,6 @@ void ECMAScriptFunctionObject::visit_edges(Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_environment); visitor.visit(m_private_environment); - visitor.visit(m_realm); visitor.visit(m_home_object); visitor.visit(m_name_string); @@ -694,27 +692,13 @@ ThrowCompletionOr ECMAScriptFunctionObject::prepare_for_ordinary_call(Exec // 1. Let callerContext be the running execution context. // 2. Let calleeContext be a new ECMAScript code execution context. - // NOTE: In the specification, PrepareForOrdinaryCall "returns" a new callee execution context. - // To avoid heap allocations, we put our ExecutionContext objects on the C++ stack instead. - // Whoever calls us should put an ExecutionContext on their stack and pass that as the `callee_context`. - // 3. Set the Function of calleeContext to F. callee_context.function = this; callee_context.function_name = m_name_string; // 4. Let calleeRealm be F.[[Realm]]. - auto callee_realm = m_realm; - // NOTE: This non-standard fallback is needed until we can guarantee that literally - // every function has a realm - especially in LibWeb that's sometimes not the case - // when a function is created while no JS is running, as we currently need to rely on - // that (:acid2:, I know - see set_event_handler_attribute() for an example). - // If there's no 'current realm' either, we can't continue and crash. - if (!callee_realm) - callee_realm = vm.current_realm(); - VERIFY(callee_realm); - // 5. Set the Realm of calleeContext to calleeRealm. - callee_context.realm = callee_realm; + callee_context.realm = realm(); // 6. Set the ScriptOrModule of calleeContext to F.[[ScriptOrModule]]. callee_context.script_or_module = m_script_or_module; @@ -758,15 +742,7 @@ void ECMAScriptFunctionObject::ordinary_call_bind_this(ExecutionContext& callee_ return; // 3. Let calleeRealm be F.[[Realm]]. - auto callee_realm = m_realm; - // NOTE: This non-standard fallback is needed until we can guarantee that literally - // every function has a realm - especially in LibWeb that's sometimes not the case - // when a function is created while no JS is running, as we currently need to rely on - // that (:acid2:, I know - see set_event_handler_attribute() for an example). - // If there's no 'current realm' either, we can't continue and crash. - if (!callee_realm) - callee_realm = vm.current_realm(); - VERIFY(callee_realm); + auto callee_realm = realm(); // 4. Let localEnv be the LexicalEnvironment of calleeContext. auto local_env = callee_context.lexical_environment; diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h index 47c6397d14b..0bfd51aa4dd 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h @@ -135,7 +135,7 @@ public: auto& bytecode_executable() const { return m_bytecode_executable; } Environment* environment() { return m_environment; } - virtual Realm* realm() const override { return m_realm; } + virtual Realm* realm() const override { return &shape().realm(); } [[nodiscard]] ConstructorKind constructor_kind() const { return shared_data().m_constructor_kind; } void set_constructor_kind(ConstructorKind constructor_kind) { const_cast(shared_data()).m_constructor_kind = constructor_kind; } @@ -210,7 +210,6 @@ private: // Internal Slots of ECMAScript Function Objects, https://tc39.es/ecma262/#table-internal-slots-of-ecmascript-function-objects GC::Ptr m_environment; // [[Environment]] GC::Ptr m_private_environment; // [[PrivateEnvironment]] - GC::Ptr m_realm; // [[Realm]] ScriptOrModule m_script_or_module; // [[ScriptOrModule]] GC::Ptr m_home_object; // [[HomeObject]] struct ClassData { From 2aa6d7636cc06acdb66ffd17e1248191ced5c4f3 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 12 Apr 2025 11:44:41 +0200 Subject: [PATCH 83/83] LibWeb: Invalidate sheet owners after mutating cssText of its rules This fixes one source of flakiness on WPT (of many) where we wouldn't recompute style after programmatically altering the contents of a style sheet, but instead had to wait for something else to cause invalidation. --- Libraries/LibWeb/CSS/CSSStyleProperties.cpp | 7 + Libraries/LibWeb/DOM/Node.h | 1 + .../css/css-cascade/all-prop-revert-layer.txt | 203 ++++++++ .../css-cascade/all-prop-revert-layer.html | 470 ++++++++++++++++++ 4 files changed, 681 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html diff --git a/Libraries/LibWeb/CSS/CSSStyleProperties.cpp b/Libraries/LibWeb/CSS/CSSStyleProperties.cpp index 64f622cda43..3af27b22b93 100644 --- a/Libraries/LibWeb/CSS/CSSStyleProperties.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleProperties.cpp @@ -1158,6 +1158,13 @@ WebIDL::ExceptionOr CSSStyleProperties::set_css_text(StringView css_text) // 4. Update style attribute for the CSS declaration block. update_style_attribute(); + // Non-standard: Invalidate style for the owners of our containing sheet, if any. + if (auto rule = parent_rule()) { + if (auto sheet = rule->parent_style_sheet()) { + sheet->invalidate_owners(DOM::StyleInvalidationReason::CSSStylePropertiesTextChange); + } + } + return {}; } diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index 56f85c41a4a..e3ec675c27b 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -53,6 +53,7 @@ enum class ShouldComputeRole { X(AdoptedStyleSheetsList) \ X(CSSFontLoaded) \ X(CSSImportRule) \ + X(CSSStylePropertiesTextChange) \ X(CustomElementStateChange) \ X(DidLoseFocus) \ X(DidReceiveFocus) \ diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt new file mode 100644 index 00000000000..535648fbe77 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt @@ -0,0 +1,203 @@ +Harness status: OK + +Found 197 tests + +187 Pass +10 Fail +Pass accent-color +Pass border-collapse +Pass border-spacing +Pass caption-side +Pass caret-color +Pass clip-rule +Pass color +Pass color-scheme +Pass cursor +Pass direction +Pass fill +Pass fill-opacity +Pass fill-rule +Pass font-family +Pass font-feature-settings +Pass font-language-override +Pass font-size +Pass font-style +Pass font-variant-alternates +Pass font-variant-caps +Pass font-variant-east-asian +Pass font-variant-emoji +Pass font-variant-ligatures +Pass font-variant-numeric +Pass font-variant-position +Pass font-variation-settings +Pass font-weight +Pass font-width +Pass image-rendering +Pass letter-spacing +Pass line-height +Pass list-style-position +Pass list-style-type +Pass math-depth +Pass math-shift +Pass math-style +Pass pointer-events +Pass quotes +Pass stroke +Pass stroke-dasharray +Pass stroke-dashoffset +Pass stroke-linecap +Pass stroke-linejoin +Pass stroke-miterlimit +Pass stroke-opacity +Pass stroke-width +Pass tab-size +Pass text-align +Pass text-anchor +Pass text-decoration-line +Pass text-indent +Pass text-justify +Pass text-shadow +Pass text-transform +Pass visibility +Pass white-space +Pass word-break +Pass word-spacing +Pass word-wrap +Pass writing-mode +Pass align-items +Pass align-self +Pass animation-delay +Pass animation-direction +Pass animation-duration +Pass animation-fill-mode +Pass animation-iteration-count +Fail animation-name +Pass animation-play-state +Pass animation-timing-function +Pass appearance +Pass aspect-ratio +Pass backdrop-filter +Pass background-attachment +Pass background-blend-mode +Pass background-clip +Pass background-color +Pass background-origin +Pass background-repeat +Pass background-size +Fail block-size +Pass bottom +Pass box-shadow +Pass box-sizing +Pass clear +Pass clip +Pass clip-path +Pass column-count +Pass column-gap +Pass column-span +Pass column-width +Pass contain +Pass content +Pass content-visibility +Pass counter-increment +Pass counter-reset +Pass counter-set +Pass cx +Pass cy +Pass display +Pass flex-basis +Pass flex-direction +Pass flex-grow +Pass flex-shrink +Pass flex-wrap +Pass float +Pass grid-auto-columns +Pass grid-auto-flow +Pass grid-auto-rows +Pass grid-column-end +Pass grid-column-start +Pass grid-row-end +Pass grid-row-start +Pass grid-template-areas +Pass grid-template-columns +Pass grid-template-rows +Fail height +Fail inline-size +Pass inset-block-end +Pass inset-block-start +Pass inset-inline-end +Pass inset-inline-start +Pass isolation +Pass justify-content +Pass justify-items +Pass justify-self +Pass left +Pass margin-block-end +Pass margin-block-start +Pass margin-bottom +Pass margin-inline-end +Pass margin-inline-start +Pass margin-left +Pass margin-right +Pass margin-top +Pass mask-image +Pass mask-type +Fail max-block-size +Pass max-height +Fail max-inline-size +Pass max-width +Fail min-block-size +Pass min-height +Fail min-inline-size +Pass min-width +Pass mix-blend-mode +Pass object-fit +Pass object-position +Pass opacity +Pass order +Pass outline-color +Pass outline-offset +Pass outline-style +Pass outline-width +Pass overflow-x +Pass overflow-y +Pass padding-block-end +Pass padding-block-start +Pass padding-bottom +Pass padding-inline-end +Pass padding-inline-start +Pass padding-left +Pass padding-right +Pass padding-top +Pass position +Pass r +Pass right +Pass rotate +Pass row-gap +Pass rx +Pass ry +Pass scale +Pass scrollbar-gutter +Pass scrollbar-width +Pass stop-color +Pass stop-opacity +Pass table-layout +Pass text-decoration-color +Pass text-decoration-style +Pass text-decoration-thickness +Pass text-overflow +Pass top +Fail transform +Pass transform-box +Pass transition-delay +Pass transition-duration +Pass transition-property +Pass transition-timing-function +Pass translate +Pass unicode-bidi +Pass user-select +Pass vertical-align +Pass view-transition-name +Fail width +Pass x +Pass y +Pass z-index \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html b/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html new file mode 100644 index 00000000000..5d1d08dfb8c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html @@ -0,0 +1,470 @@ + + +CSS Cascade: "all: revert-layer" + + + + + +
+ + + + + + +