diff --git a/.github/workflows/js-and-wasm-artifacts.yml b/.github/workflows/js-and-wasm-artifacts.yml index 77a33e56f5b..d7a62f749f4 100644 --- a/.github/workflows/js-and-wasm-artifacts.yml +++ b/.github/workflows/js-and-wasm-artifacts.yml @@ -49,6 +49,19 @@ jobs: with: ref: ${{ inputs.reference_to_build }} + - name: 'Determine build commit hash' + id: build-commit + shell: bash + run: | + echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + + - name: 'Use latest custom actions' + if: ${{ inputs.reference_to_build != '' && github.sha != steps.build-commit.outputs.sha }} + shell: bash + run: | + git fetch origin master + git checkout origin/master -- .github/actions + - name: "Set up environment" uses: ./.github/actions/setup with: @@ -102,6 +115,17 @@ jobs: run: | cpack + # Inject the COMMIT file for older builds (before commit 5c5de0e30e04). + for package in ladybird-*.tar.gz; do + if ! tar -tzf "${package}" | grep -qx COMMIT; then + echo "${{ steps.build-commit.outputs.sha }}" > COMMIT + gunzip "${package}" + tar --append --file="${package%.gz}" COMMIT + gzip "${package%.gz}" + rm COMMIT + fi + done + - name: Save Caches uses: ./.github/actions/cache-save with: @@ -115,23 +139,27 @@ jobs: - name: Sanity-check the js repl shell: bash run: | - set -e - tar -xvzf Build/distribution/ladybird-js-${{ matrix.package_type }}.tar.gz - ./bin/js -c "console.log('Hello, World\!');" > js-repl-out.txt - if ! grep -q "\"Hello, World\!\"" js-repl-out.txt; then + path="Build/distribution/ladybird-js-${{ matrix.package_type }}.tar.gz" + if [ -f "${path}" ]; then + tar -xvzf "${path}" + bin/js -c "console.log('Hello, World\!');" > js-repl-out.txt + if ! grep -q "\"Hello, World\!\"" js-repl-out.txt; then echo "Sanity check failed: \"Hello, World\!\" not found in output." exit 1 + fi fi - name: Sanity-check the wasm repl shell: bash run: | - set -e - tar -xvzf Build/distribution/ladybird-wasm-${{ matrix.package_type }}.tar.gz - ./bin/wasm -e run_sanity_check -w ${{ github.workspace }}/Libraries/LibWasm/Tests/CI/ci-sanity-check.wasm > wasm-repl-out.txt - if ! grep -q "Hello, World\!" wasm-repl-out.txt; then + path="Build/distribution/ladybird-wasm-${{ matrix.package_type }}.tar.gz" + if [ -f "${path}" ]; then + tar -xvzf "${path}" + bin/wasm -e run_sanity_check -w ${{ github.workspace }}/Libraries/LibWasm/Tests/CI/ci-sanity-check.wasm > wasm-repl-out.txt + if ! grep -q "Hello, World\!" wasm-repl-out.txt; then echo "Sanity check failed: Hello, World\! not found in output." exit 1 + fi fi - name: Upload js package diff --git a/Libraries/CMakeLists.txt b/Libraries/CMakeLists.txt index f823ce69124..76ce2735c2d 100644 --- a/Libraries/CMakeLists.txt +++ b/Libraries/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(LibCompress) add_subdirectory(LibCrypto) +add_subdirectory(LibDatabase) add_subdirectory(LibDiff) add_subdirectory(LibDNS) add_subdirectory(LibGC) diff --git a/Libraries/LibCore/StandardPaths.cpp b/Libraries/LibCore/StandardPaths.cpp index cab1d2c204e..911061f3f61 100644 --- a/Libraries/LibCore/StandardPaths.cpp +++ b/Libraries/LibCore/StandardPaths.cpp @@ -129,6 +129,27 @@ ByteString StandardPaths::videos_directory() return LexicalPath::canonicalized_path(builder.to_byte_string()); } +ByteString StandardPaths::cache_directory() +{ +#if defined(AK_OS_WINDOWS) || defined(AK_OS_HAIKU) + return user_data_directory(); +#else + if (auto cache_directory = get_environment_if_not_empty("XDG_CACHE_HOME"sv); cache_directory.has_value()) + return LexicalPath::canonicalized_path(*cache_directory); + + StringBuilder builder; + builder.append(home_directory()); + +# if defined(AK_OS_MACOS) + builder.append("/Library/Caches"sv); +# else + builder.append("/.cache"sv); +# endif + + return LexicalPath::canonicalized_path(builder.to_byte_string()); +#endif +} + ByteString StandardPaths::config_directory() { StringBuilder builder; diff --git a/Libraries/LibCore/StandardPaths.h b/Libraries/LibCore/StandardPaths.h index 028f860027a..30fa4de8c1a 100644 --- a/Libraries/LibCore/StandardPaths.h +++ b/Libraries/LibCore/StandardPaths.h @@ -22,6 +22,7 @@ public: static ByteString pictures_directory(); static ByteString videos_directory(); static ByteString tempfile_directory(); + static ByteString cache_directory(); static ByteString config_directory(); static ByteString user_data_directory(); static Vector system_data_directories(); diff --git a/Libraries/LibCore/System.cpp b/Libraries/LibCore/System.cpp index 59a2b9ec269..c8ed6b3cbc2 100644 --- a/Libraries/LibCore/System.cpp +++ b/Libraries/LibCore/System.cpp @@ -870,4 +870,27 @@ ErrorOr set_close_on_exec(int fd, bool enabled) return {}; } +ErrorOr transfer_file_through_pipe(int source_fd, int target_fd, size_t source_offset, size_t source_length) +{ +#if defined(AK_OS_LINUX) + auto sent = ::splice(source_fd, reinterpret_cast(&source_offset), target_fd, nullptr, source_length, SPLICE_F_MOVE | SPLICE_F_NONBLOCK); + if (sent < 0) + return Error::from_syscall("send_file_to_pipe"sv, errno); + return sent; +#else + static auto page_size = PAGE_SIZE; + + // mmap requires the offset to be page-aligned, so we must handle that here. + auto aligned_source_offset = (source_offset / page_size) * page_size; + auto offset_adjustment = source_offset - aligned_source_offset; + auto mapped_source_length = source_length + offset_adjustment; + + // FIXME: We could use MappedFile here if we update it to support offsets and not auto-close the source fd. + auto* mapped = TRY(mmap(nullptr, mapped_source_length, PROT_READ, MAP_SHARED, source_fd, aligned_source_offset)); + ScopeGuard guard { [&]() { (void)munmap(mapped, mapped_source_length); } }; + + return TRY(write(target_fd, { static_cast(mapped) + offset_adjustment, source_length })); +#endif +} + } diff --git a/Libraries/LibCore/System.h b/Libraries/LibCore/System.h index cc84c5eedb5..8ebf7603ba9 100644 --- a/Libraries/LibCore/System.h +++ b/Libraries/LibCore/System.h @@ -190,4 +190,6 @@ bool is_socket(int fd); ErrorOr sleep_ms(u32 milliseconds); ErrorOr set_close_on_exec(int fd, bool enabled); +ErrorOr transfer_file_through_pipe(int source_fd, int target_fd, size_t source_offset, size_t source_length); + } diff --git a/Libraries/LibCore/SystemWindows.cpp b/Libraries/LibCore/SystemWindows.cpp index 289551730c5..7a0eda67b03 100644 --- a/Libraries/LibCore/SystemWindows.cpp +++ b/Libraries/LibCore/SystemWindows.cpp @@ -403,4 +403,14 @@ ErrorOr kill(pid_t pid, int signal) return {}; } +ErrorOr transfer_file_through_pipe(int source_fd, int target_fd, size_t source_offset, size_t source_length) +{ + (void)source_fd; + (void)target_fd; + (void)source_offset; + (void)source_length; + + return Error::from_string_literal("FIXME: Implement System::transfer_file_through_pipe on Windows (for HTTP disk cache)"); +} + } diff --git a/Libraries/LibDatabase/CMakeLists.txt b/Libraries/LibDatabase/CMakeLists.txt new file mode 100644 index 00000000000..d30f6cec0b6 --- /dev/null +++ b/Libraries/LibDatabase/CMakeLists.txt @@ -0,0 +1,8 @@ +set(SOURCES + Database.cpp +) + +find_package(SQLite3 REQUIRED) + +ladybird_lib(LibDatabase database EXPLICIT_SYMBOL_EXPORT) +target_link_libraries(LibDatabase PRIVATE LibCore SQLite::SQLite3) diff --git a/Libraries/LibWebView/Database.cpp b/Libraries/LibDatabase/Database.cpp similarity index 64% rename from Libraries/LibWebView/Database.cpp rename to Libraries/LibDatabase/Database.cpp index e78174a38ba..faed11b2022 100644 --- a/Libraries/LibWebView/Database.cpp +++ b/Libraries/LibDatabase/Database.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2025, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,12 +8,11 @@ #include #include #include -#include -#include +#include #include -namespace WebView { +namespace Database { static constexpr StringView sql_error(int error_code) { @@ -39,13 +38,25 @@ static constexpr StringView sql_error(int error_code) } \ }) -ErrorOr> Database::create() -{ - // FIXME: Move this to a generic "Ladybird data directory" helper. - auto database_path = ByteString::formatted("{}/Ladybird", Core::StandardPaths::user_data_directory()); - TRY(Core::Directory::create(database_path, Core::Directory::CreateDirectories::Yes)); +#define ENUMERATE_SQL_TYPES \ + __ENUMERATE_TYPE(String) \ + __ENUMERATE_TYPE(UnixDateTime) \ + __ENUMERATE_TYPE(i8) \ + __ENUMERATE_TYPE(i16) \ + __ENUMERATE_TYPE(i32) \ + __ENUMERATE_TYPE(long) \ + __ENUMERATE_TYPE(long long) \ + __ENUMERATE_TYPE(u8) \ + __ENUMERATE_TYPE(u16) \ + __ENUMERATE_TYPE(u32) \ + __ENUMERATE_TYPE(unsigned long) \ + __ENUMERATE_TYPE(unsigned long long) \ + __ENUMERATE_TYPE(bool) - auto database_file = ByteString::formatted("{}/Ladybird.db", database_path); +ErrorOr> Database::create(ByteString const& directory, StringView name) +{ + TRY(Core::Directory::create(directory, Core::Directory::CreateDirectories::Yes)); + auto database_file = ByteString::formatted("{}/{}.db", directory, name); sqlite3* m_database { nullptr }; SQL_TRY(sqlite3_open(database_file.characters(), &m_database)); @@ -67,7 +78,7 @@ Database::~Database() sqlite3_close(m_database); } -ErrorOr Database::prepare_statement(StringView statement) +ErrorOr Database::prepare_statement(StringView statement) { sqlite3_stmt* prepared_statement { nullptr }; SQL_TRY(sqlite3_prepare_v2(m_database, statement.characters_without_null_termination(), static_cast(statement.length()), &prepared_statement, nullptr)); @@ -111,18 +122,21 @@ void Database::apply_placeholder(StatementID statement_id, int index, ValueType StringView string { value }; SQL_MUST(sqlite3_bind_text(statement, index, string.characters_without_null_termination(), static_cast(string.length()), SQLITE_TRANSIENT)); } else if constexpr (IsSame) { - SQL_MUST(sqlite3_bind_int64(statement, index, value.offset_to_epoch().to_milliseconds())); - } else if constexpr (IsSame) { - SQL_MUST(sqlite3_bind_int(statement, index, value)); - } else if constexpr (IsSame) { - SQL_MUST(sqlite3_bind_int(statement, index, static_cast(value))); + apply_placeholder(statement_id, index, value.offset_to_epoch().to_milliseconds()); + } else if constexpr (IsIntegral) { + if constexpr (sizeof(ValueType) <= sizeof(int)) + SQL_MUST(sqlite3_bind_int(statement, index, static_cast(value))); + else + SQL_MUST(sqlite3_bind_int64(statement, index, static_cast(value))); + } else { + static_assert(DependentFalse); } } -template void Database::apply_placeholder(StatementID, int, String const&); -template void Database::apply_placeholder(StatementID, int, UnixDateTime const&); -template void Database::apply_placeholder(StatementID, int, int const&); -template void Database::apply_placeholder(StatementID, int, bool const&); +#define __ENUMERATE_TYPE(type) \ + template DATABASE_API void Database::apply_placeholder(StatementID, int, type const&); +ENUMERATE_SQL_TYPES +#undef __ENUMERATE_TYPE template ValueType Database::result_column(StatementID statement_id, int column) @@ -133,20 +147,21 @@ ValueType Database::result_column(StatementID statement_id, int column) auto const* text = reinterpret_cast(sqlite3_column_text(statement, column)); return MUST(String::from_utf8(StringView { text, strlen(text) })); } else if constexpr (IsSame) { - auto milliseconds = sqlite3_column_int64(statement, column); + auto milliseconds = result_column(statement_id, column); return UnixDateTime::from_milliseconds_since_epoch(milliseconds); - } else if constexpr (IsSame) { - return sqlite3_column_int(statement, column); - } else if constexpr (IsSame) { - return static_cast(sqlite3_column_int(statement, column)); + } else if constexpr (IsIntegral) { + if constexpr (sizeof(ValueType) <= sizeof(int)) + return static_cast(sqlite3_column_int(statement, column)); + else + return static_cast(sqlite3_column_int64(statement, column)); + } else { + static_assert(DependentFalse); } - - VERIFY_NOT_REACHED(); } -template String Database::result_column(StatementID, int); -template UnixDateTime Database::result_column(StatementID, int); -template int Database::result_column(StatementID, int); -template bool Database::result_column(StatementID, int); +#define __ENUMERATE_TYPE(type) \ + template DATABASE_API type Database::result_column(StatementID, int); +ENUMERATE_SQL_TYPES +#undef __ENUMERATE_TYPE } diff --git a/Libraries/LibWebView/Database.h b/Libraries/LibDatabase/Database.h similarity index 83% rename from Libraries/LibWebView/Database.h rename to Libraries/LibDatabase/Database.h index 044211a5b45..0c105e6628e 100644 --- a/Libraries/LibWebView/Database.h +++ b/Libraries/LibDatabase/Database.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2025, Tim Flynn * Copyright (c) 2023, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause @@ -13,19 +13,18 @@ #include #include #include -#include +#include struct sqlite3; struct sqlite3_stmt; -namespace WebView { +namespace Database { -class WEBVIEW_API Database : public RefCounted { +class DATABASE_API Database : public RefCounted { public: - static ErrorOr> create(); + static ErrorOr> create(ByteString const& directory, StringView name); ~Database(); - using StatementID = size_t; using OnResult = Function; ErrorOr prepare_statement(StringView statement); diff --git a/Libraries/LibDatabase/Forward.h b/Libraries/LibDatabase/Forward.h new file mode 100644 index 00000000000..a5afc01bd8a --- /dev/null +++ b/Libraries/LibDatabase/Forward.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Database { + +class Database; + +using StatementID = size_t; + +} diff --git a/Libraries/LibGfx/Bitmap.cpp b/Libraries/LibGfx/Bitmap.cpp index 8f0a2a4aea6..c5b0a98abcd 100644 --- a/Libraries/LibGfx/Bitmap.cpp +++ b/Libraries/LibGfx/Bitmap.cpp @@ -10,6 +10,11 @@ #include #include #include +#include + +#include +#include +#include #include #ifdef AK_OS_MACOS @@ -184,6 +189,24 @@ ErrorOr> Bitmap::cropped(Gfx::IntRect crop, Gfx::Colo return new_bitmap; } +ErrorOr> Bitmap::scaled(int const width, int const height, ScalingMode const scaling_mode) const +{ + auto const source_info = SkImageInfo::Make(this->width(), this->height(), to_skia_color_type(format()), to_skia_alpha_type(format(), alpha_type()), nullptr); + SkPixmap const source_sk_pixmap(source_info, begin(), pitch()); + SkBitmap source_sk_bitmap; + source_sk_bitmap.installPixels(source_sk_pixmap); + source_sk_bitmap.setImmutable(); + + auto scaled_bitmap = TRY(Gfx::Bitmap::create(format(), alpha_type(), { width, height })); + auto const scaled_info = SkImageInfo::Make(scaled_bitmap->width(), scaled_bitmap->height(), to_skia_color_type(scaled_bitmap->format()), to_skia_alpha_type(scaled_bitmap->format(), scaled_bitmap->alpha_type()), nullptr); + SkPixmap const scaled_sk_pixmap(scaled_info, scaled_bitmap->begin(), scaled_bitmap->pitch()); + + sk_sp source_sk_image = source_sk_bitmap.asImage(); + if (!source_sk_image->scalePixels(scaled_sk_pixmap, to_skia_sampling_options(scaling_mode))) + return Error::from_string_literal("Unable to scale pixels for bitmap"); + return scaled_bitmap; +} + ErrorOr> Bitmap::to_bitmap_backed_by_anonymous_buffer() const { if (m_buffer.is_valid()) { diff --git a/Libraries/LibGfx/Bitmap.h b/Libraries/LibGfx/Bitmap.h index 682becefd70..31c7aa5619d 100644 --- a/Libraries/LibGfx/Bitmap.h +++ b/Libraries/LibGfx/Bitmap.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace Gfx { @@ -74,6 +75,8 @@ public: ErrorOr> clone() const; ErrorOr> cropped(Gfx::IntRect, Gfx::Color outside_color = Gfx::Color::Black) const; + ErrorOr> scaled(int width, int height, ScalingMode scaling_mode) const; + ErrorOr> to_bitmap_backed_by_anonymous_buffer() const; [[nodiscard]] ShareableBitmap to_shareable_bitmap() const; diff --git a/Libraries/LibRequests/NetworkError.h b/Libraries/LibRequests/NetworkError.h index 5250d99e318..d2ca83bbe5c 100644 --- a/Libraries/LibRequests/NetworkError.h +++ b/Libraries/LibRequests/NetworkError.h @@ -21,6 +21,7 @@ enum class NetworkError { MalformedUrl, InvalidContentEncoding, RequestServerDied, + CacheReadFailed, Unknown, }; @@ -47,6 +48,8 @@ constexpr StringView network_error_to_string(NetworkError network_error) return "Response could not be decoded with its Content-Encoding"sv; case NetworkError::RequestServerDied: return "RequestServer is currently unavailable"sv; + case NetworkError::CacheReadFailed: + return "RequestServer encountered an error reading a cached HTTP response"sv; case NetworkError::Unknown: return "An unexpected network error occurred"sv; } diff --git a/Libraries/LibWeb/CSS/CSSImageValue.cpp b/Libraries/LibWeb/CSS/CSSImageValue.cpp index 857931cd5bb..f6fe3a237f9 100644 --- a/Libraries/LibWeb/CSS/CSSImageValue.cpp +++ b/Libraries/LibWeb/CSS/CSSImageValue.cpp @@ -40,7 +40,7 @@ WebIDL::ExceptionOr CSSImageValue::to_string() const } // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation -WebIDL::ExceptionOr> CSSImageValue::create_an_internal_representation(PropertyNameAndID const& property) const +WebIDL::ExceptionOr> CSSImageValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const { // If value is a CSSStyleValue subclass, // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. @@ -53,7 +53,7 @@ WebIDL::ExceptionOr> CSSImageValue::create_an_in } return property_accepts_type(property.id(), ValueType::Image); }(); - if (!matches_grammar) { + if (perform_type_check == PerformTypeCheck::Yes && !matches_grammar) { return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Property '{}' does not accept ", property.name())) }; } diff --git a/Libraries/LibWeb/CSS/CSSImageValue.h b/Libraries/LibWeb/CSS/CSSImageValue.h index 84c6a2e81a3..6c40f75fc98 100644 --- a/Libraries/LibWeb/CSS/CSSImageValue.h +++ b/Libraries/LibWeb/CSS/CSSImageValue.h @@ -21,7 +21,7 @@ public: virtual ~CSSImageValue() override = default; virtual WebIDL::ExceptionOr to_string() const override; - virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&) const override; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override; private: explicit CSSImageValue(JS::Realm&, NonnullRefPtr source_value); diff --git a/Libraries/LibWeb/CSS/CSSKeywordValue.cpp b/Libraries/LibWeb/CSS/CSSKeywordValue.cpp index a5fb7eacd2c..97d2036a7c8 100644 --- a/Libraries/LibWeb/CSS/CSSKeywordValue.cpp +++ b/Libraries/LibWeb/CSS/CSSKeywordValue.cpp @@ -68,7 +68,7 @@ WebIDL::ExceptionOr CSSKeywordValue::to_string() const } // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation -WebIDL::ExceptionOr> CSSKeywordValue::create_an_internal_representation(PropertyNameAndID const& property) const +WebIDL::ExceptionOr> CSSKeywordValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const { // If value is a CSSStyleValue subclass, // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. @@ -87,7 +87,7 @@ WebIDL::ExceptionOr> CSSKeywordValue::create_an_ auto keyword = keyword_from_string(m_value); return keyword.has_value() && property_accepts_keyword(property.id(), keyword.value()); }(); - if (!matches_grammar) { + if (perform_type_check == PerformTypeCheck::Yes && !matches_grammar) { return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Property '{}' does not accept the keyword '{}'", property.name(), m_value)) }; } diff --git a/Libraries/LibWeb/CSS/CSSKeywordValue.h b/Libraries/LibWeb/CSS/CSSKeywordValue.h index 1ede8bd0c44..a292214507e 100644 --- a/Libraries/LibWeb/CSS/CSSKeywordValue.h +++ b/Libraries/LibWeb/CSS/CSSKeywordValue.h @@ -29,7 +29,7 @@ public: WebIDL::ExceptionOr set_value(FlyString value); virtual WebIDL::ExceptionOr to_string() const override; - virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&) const override; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override; private: explicit CSSKeywordValue(JS::Realm&, FlyString value); diff --git a/Libraries/LibWeb/CSS/CSSMathValue.cpp b/Libraries/LibWeb/CSS/CSSMathValue.cpp index 4cc6e7965b7..a5f8fbe0f7b 100644 --- a/Libraries/LibWeb/CSS/CSSMathValue.cpp +++ b/Libraries/LibWeb/CSS/CSSMathValue.cpp @@ -26,7 +26,7 @@ void CSSMathValue::initialize(JS::Realm& realm) } // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation -WebIDL::ExceptionOr> CSSMathValue::create_an_internal_representation(PropertyNameAndID const& property) const +WebIDL::ExceptionOr> CSSMathValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const { // If value is a CSSStyleValue subclass, // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. @@ -59,7 +59,7 @@ WebIDL::ExceptionOr> CSSMathValue::create_an_int return false; }(); - if (!matches) + if (perform_type_check == PerformTypeCheck::Yes && !matches) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv }; return CalculatedStyleValue::create(TRY(create_calculation_node(context)), type(), move(context)); diff --git a/Libraries/LibWeb/CSS/CSSMathValue.h b/Libraries/LibWeb/CSS/CSSMathValue.h index 54d33c4b000..4651db7028f 100644 --- a/Libraries/LibWeb/CSS/CSSMathValue.h +++ b/Libraries/LibWeb/CSS/CSSMathValue.h @@ -32,7 +32,7 @@ public: }; virtual String serialize_math_value(Nested, Parens) const = 0; - virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&) const final override; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const final override; protected: explicit CSSMathValue(JS::Realm&, Bindings::CSSMathOperator, NumericType); diff --git a/Libraries/LibWeb/CSS/CSSMatrixComponent.cpp b/Libraries/LibWeb/CSS/CSSMatrixComponent.cpp index 6b8907a6a20..12ae85862b8 100644 --- a/Libraries/LibWeb/CSS/CSSMatrixComponent.cpp +++ b/Libraries/LibWeb/CSS/CSSMatrixComponent.cpp @@ -7,6 +7,9 @@ #include "CSSMatrixComponent.h" #include #include +#include +#include +#include #include #include @@ -82,4 +85,39 @@ WebIDL::ExceptionOr CSSMatrixComponent::set_matrix(GC::Ref> CSSMatrixComponent::create_style_value(PropertyNameAndID const& property) const +{ + if (is_2d()) { + return TransformationStyleValue::create(property.id(), TransformFunction::Matrix, + { + NumberStyleValue::create(m_matrix->a()), + NumberStyleValue::create(m_matrix->b()), + NumberStyleValue::create(m_matrix->c()), + NumberStyleValue::create(m_matrix->d()), + NumberStyleValue::create(m_matrix->e()), + NumberStyleValue::create(m_matrix->f()), + }); + } + + return TransformationStyleValue::create(property.id(), TransformFunction::Matrix3d, + { + NumberStyleValue::create(m_matrix->m11()), + NumberStyleValue::create(m_matrix->m12()), + NumberStyleValue::create(m_matrix->m13()), + NumberStyleValue::create(m_matrix->m14()), + NumberStyleValue::create(m_matrix->m21()), + NumberStyleValue::create(m_matrix->m22()), + NumberStyleValue::create(m_matrix->m23()), + NumberStyleValue::create(m_matrix->m24()), + NumberStyleValue::create(m_matrix->m31()), + NumberStyleValue::create(m_matrix->m32()), + NumberStyleValue::create(m_matrix->m33()), + NumberStyleValue::create(m_matrix->m34()), + NumberStyleValue::create(m_matrix->m41()), + NumberStyleValue::create(m_matrix->m42()), + NumberStyleValue::create(m_matrix->m43()), + NumberStyleValue::create(m_matrix->m44()), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSMatrixComponent.h b/Libraries/LibWeb/CSS/CSSMatrixComponent.h index 3389ac75e2c..f9586ffb9a3 100644 --- a/Libraries/LibWeb/CSS/CSSMatrixComponent.h +++ b/Libraries/LibWeb/CSS/CSSMatrixComponent.h @@ -33,6 +33,8 @@ public: GC::Ref matrix() const { return m_matrix; } WebIDL::ExceptionOr set_matrix(GC::Ref matrix); + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSMatrixComponent(JS::Realm&, Is2D, GC::Ref); diff --git a/Libraries/LibWeb/CSS/CSSPerspective.cpp b/Libraries/LibWeb/CSS/CSSPerspective.cpp index 73c473e2083..3a89cb7148c 100644 --- a/Libraries/LibWeb/CSS/CSSPerspective.cpp +++ b/Libraries/LibWeb/CSS/CSSPerspective.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -157,4 +159,12 @@ void CSSPerspective::set_is_2d(bool) // The is2D attribute of a CSSPerspective object must, on setting, do nothing. } +WebIDL::ExceptionOr> CSSPerspective::create_style_value(PropertyNameAndID const& property) const +{ + auto length = TRY(m_length.visit([&](auto const& value) { + return value->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No); + })); + return TransformationStyleValue::create(property.id(), TransformFunction::Perspective, { move(length) }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSPerspective.h b/Libraries/LibWeb/CSS/CSSPerspective.h index d947d5d7d06..4ea60d938a9 100644 --- a/Libraries/LibWeb/CSS/CSSPerspective.h +++ b/Libraries/LibWeb/CSS/CSSPerspective.h @@ -37,6 +37,8 @@ public: virtual void set_is_2d(bool value) override; + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSPerspective(JS::Realm&, CSSPerspectiveValueInternal); diff --git a/Libraries/LibWeb/CSS/CSSRotate.cpp b/Libraries/LibWeb/CSS/CSSRotate.cpp index e31ee913719..add84fdcd57 100644 --- a/Libraries/LibWeb/CSS/CSSRotate.cpp +++ b/Libraries/LibWeb/CSS/CSSRotate.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -218,4 +220,22 @@ WebIDL::ExceptionOr CSSRotate::set_angle(GC::Ref value) return {}; } +WebIDL::ExceptionOr> CSSRotate::create_style_value(PropertyNameAndID const& property) const +{ + if (is_2d()) { + return TransformationStyleValue::create(property.id(), TransformFunction::Rotate, + { + TRY(m_angle->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); + } + + return TransformationStyleValue::create(property.id(), TransformFunction::Rotate3d, + { + TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_z->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_angle->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSRotate.h b/Libraries/LibWeb/CSS/CSSRotate.h index 05320f8454c..21d15c3bd75 100644 --- a/Libraries/LibWeb/CSS/CSSRotate.h +++ b/Libraries/LibWeb/CSS/CSSRotate.h @@ -36,6 +36,8 @@ public: WebIDL::ExceptionOr set_z(CSSNumberish value); WebIDL::ExceptionOr set_angle(GC::Ref value); + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSRotate(JS::Realm&, Is2D, GC::Ref x, GC::Ref y, GC::Ref z, GC::Ref angle); diff --git a/Libraries/LibWeb/CSS/CSSScale.cpp b/Libraries/LibWeb/CSS/CSSScale.cpp index d2f43dc96ce..d5c729d02df 100644 --- a/Libraries/LibWeb/CSS/CSSScale.cpp +++ b/Libraries/LibWeb/CSS/CSSScale.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -199,4 +201,22 @@ WebIDL::ExceptionOr CSSScale::set_z(CSSNumberish value) return {}; } +WebIDL::ExceptionOr> CSSScale::create_style_value(PropertyNameAndID const& property) const +{ + if (is_2d()) { + return TransformationStyleValue::create(property.id(), TransformFunction::Scale, + { + TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); + } + + return TransformationStyleValue::create(property.id(), TransformFunction::Scale3d, + { + TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_z->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSScale.h b/Libraries/LibWeb/CSS/CSSScale.h index 54344756e1a..dedb519a1a6 100644 --- a/Libraries/LibWeb/CSS/CSSScale.h +++ b/Libraries/LibWeb/CSS/CSSScale.h @@ -33,6 +33,8 @@ public: WebIDL::ExceptionOr set_y(CSSNumberish value); WebIDL::ExceptionOr set_z(CSSNumberish value); + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSScale(JS::Realm&, Is2D, GC::Ref x, GC::Ref y, GC::Ref z); diff --git a/Libraries/LibWeb/CSS/CSSSkew.cpp b/Libraries/LibWeb/CSS/CSSSkew.cpp index 28c73991b7a..51c4129cd5e 100644 --- a/Libraries/LibWeb/CSS/CSSSkew.cpp +++ b/Libraries/LibWeb/CSS/CSSSkew.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -136,4 +138,13 @@ void CSSSkew::set_is_2d(bool) // The is2D attribute of a CSSSkew, CSSSkewX, or CSSSkewY object must, on setting, do nothing. } +WebIDL::ExceptionOr> CSSSkew::create_style_value(PropertyNameAndID const& property) const +{ + return TransformationStyleValue::create(property.id(), TransformFunction::Skew, + { + TRY(m_ax->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_ay->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSSkew.h b/Libraries/LibWeb/CSS/CSSSkew.h index a876e548b18..e4d4bd455be 100644 --- a/Libraries/LibWeb/CSS/CSSSkew.h +++ b/Libraries/LibWeb/CSS/CSSSkew.h @@ -32,6 +32,8 @@ public: virtual void set_is_2d(bool value) override; + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSSkew(JS::Realm&, GC::Ref ax, GC::Ref ay); diff --git a/Libraries/LibWeb/CSS/CSSSkewX.cpp b/Libraries/LibWeb/CSS/CSSSkewX.cpp index bcd52447d50..899d28f6842 100644 --- a/Libraries/LibWeb/CSS/CSSSkewX.cpp +++ b/Libraries/LibWeb/CSS/CSSSkewX.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -106,4 +108,12 @@ void CSSSkewX::set_is_2d(bool) // The is2D attribute of a CSSSkewX, CSSSkewXX, or CSSSkewXY object must, on setting, do nothing. } +WebIDL::ExceptionOr> CSSSkewX::create_style_value(PropertyNameAndID const& property) const +{ + return TransformationStyleValue::create(property.id(), TransformFunction::SkewX, + { + TRY(m_ax->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSSkewX.h b/Libraries/LibWeb/CSS/CSSSkewX.h index 23c677400b2..4507cee5bb3 100644 --- a/Libraries/LibWeb/CSS/CSSSkewX.h +++ b/Libraries/LibWeb/CSS/CSSSkewX.h @@ -30,6 +30,8 @@ public: virtual void set_is_2d(bool value) override; + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSSkewX(JS::Realm&, GC::Ref ax); diff --git a/Libraries/LibWeb/CSS/CSSSkewY.cpp b/Libraries/LibWeb/CSS/CSSSkewY.cpp index 4906ba345ca..ec1f4ed5be0 100644 --- a/Libraries/LibWeb/CSS/CSSSkewY.cpp +++ b/Libraries/LibWeb/CSS/CSSSkewY.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -106,4 +108,12 @@ void CSSSkewY::set_is_2d(bool) // The is2D attribute of a CSSSkewY, CSSSkewYX, or CSSSkewYY object must, on setting, do nothing. } +WebIDL::ExceptionOr> CSSSkewY::create_style_value(PropertyNameAndID const& property) const +{ + return TransformationStyleValue::create(property.id(), TransformFunction::SkewY, + { + TRY(m_ay->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSSkewY.h b/Libraries/LibWeb/CSS/CSSSkewY.h index e443dd55a97..c6f58104329 100644 --- a/Libraries/LibWeb/CSS/CSSSkewY.h +++ b/Libraries/LibWeb/CSS/CSSSkewY.h @@ -30,6 +30,8 @@ public: virtual void set_is_2d(bool value) override; + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSSkewY(JS::Realm&, GC::Ref ay); diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.cpp b/Libraries/LibWeb/CSS/CSSStyleValue.cpp index 9d43229845a..883a42e5357 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleValue.cpp @@ -123,7 +123,7 @@ WebIDL::ExceptionOr CSSStyleValue::to_string() const } // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation -WebIDL::ExceptionOr> CSSStyleValue::create_an_internal_representation(PropertyNameAndID const&) const +WebIDL::ExceptionOr> CSSStyleValue::create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const { // If value is a direct CSSStyleValue, // Return value’s associated value. diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.h b/Libraries/LibWeb/CSS/CSSStyleValue.h index 7c479de4952..281f00bc451 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.h +++ b/Libraries/LibWeb/CSS/CSSStyleValue.h @@ -37,7 +37,12 @@ public: virtual WebIDL::ExceptionOr to_string() const; - virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&) const; + // FIXME: Temporary hack. Really we want to pass something like a CalculationContext with the valid types and ranges. + enum class PerformTypeCheck : u8 { + No, + Yes, + }; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const; protected: explicit CSSStyleValue(JS::Realm&); diff --git a/Libraries/LibWeb/CSS/CSSTransformComponent.h b/Libraries/LibWeb/CSS/CSSTransformComponent.h index ffabca6d66e..1b8575cbab8 100644 --- a/Libraries/LibWeb/CSS/CSSTransformComponent.h +++ b/Libraries/LibWeb/CSS/CSSTransformComponent.h @@ -31,6 +31,8 @@ public: virtual WebIDL::ExceptionOr> to_matrix() const = 0; + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const = 0; + protected: explicit CSSTransformComponent(JS::Realm&, Is2D is_2d); diff --git a/Libraries/LibWeb/CSS/CSSTransformValue.cpp b/Libraries/LibWeb/CSS/CSSTransformValue.cpp index 717fb86d00f..a039eb12e18 100644 --- a/Libraries/LibWeb/CSS/CSSTransformValue.cpp +++ b/Libraries/LibWeb/CSS/CSSTransformValue.cpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include #include @@ -155,4 +158,35 @@ WebIDL::ExceptionOr CSSTransformValue::to_string() const return builder.to_string_without_validation(); } +// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation +WebIDL::ExceptionOr> CSSTransformValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const +{ + // NB: This can become or , and we don't know which is wanted without performing the type checking. + // We can worry about that if and when we ever do have a CSSTransformValue that isn't top-level. + VERIFY(perform_type_check == PerformTypeCheck::Yes); + + // If value is a CSSStyleValue subclass, + // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. + // + // If any component of property’s CSS grammar has a limited numeric range, and the corresponding part of value + // is a CSSUnitValue that is outside of that range, replace that value with the result of wrapping it in a + // fresh CSSMathSum whose values internal slot contains only that part of value. + // + // Return the value. + + // NB: We match if we have 1 transform. We match always. + if (m_transforms.size() == 1 && property_accepts_type(property.id(), ValueType::TransformFunction)) + return TRY(m_transforms.first()->create_style_value(property)); + + if (property_accepts_type(property.id(), ValueType::TransformList)) { + StyleValueVector transforms; + transforms.ensure_capacity(m_transforms.size()); + for (auto const transform : m_transforms) + transforms.unchecked_append(TRY(transform->create_style_value(property))); + return StyleValueList::create(move(transforms), StyleValueList::Separator::Space); + } + + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv }; +} + } diff --git a/Libraries/LibWeb/CSS/CSSTransformValue.h b/Libraries/LibWeb/CSS/CSSTransformValue.h index 986d575d28f..fa38720c907 100644 --- a/Libraries/LibWeb/CSS/CSSTransformValue.h +++ b/Libraries/LibWeb/CSS/CSSTransformValue.h @@ -32,6 +32,8 @@ public: virtual WebIDL::ExceptionOr to_string() const override; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override; + private: explicit CSSTransformValue(JS::Realm&, Vector>); diff --git a/Libraries/LibWeb/CSS/CSSTranslate.cpp b/Libraries/LibWeb/CSS/CSSTranslate.cpp index 962c09da8d5..011e85cba4c 100644 --- a/Libraries/LibWeb/CSS/CSSTranslate.cpp +++ b/Libraries/LibWeb/CSS/CSSTranslate.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -178,4 +180,22 @@ WebIDL::ExceptionOr CSSTranslate::set_z(GC::Ref z) return {}; } +WebIDL::ExceptionOr> CSSTranslate::create_style_value(PropertyNameAndID const& property) const +{ + if (is_2d()) { + return TransformationStyleValue::create(property.id(), TransformFunction::Translate, + { + TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); + } + + return TransformationStyleValue::create(property.id(), TransformFunction::Translate3d, + { + TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + TRY(m_z->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)), + }); +} + } diff --git a/Libraries/LibWeb/CSS/CSSTranslate.h b/Libraries/LibWeb/CSS/CSSTranslate.h index e303b88d0c1..07dc4b064a6 100644 --- a/Libraries/LibWeb/CSS/CSSTranslate.h +++ b/Libraries/LibWeb/CSS/CSSTranslate.h @@ -32,6 +32,8 @@ public: WebIDL::ExceptionOr set_y(GC::Ref value); WebIDL::ExceptionOr set_z(GC::Ref value); + virtual WebIDL::ExceptionOr> create_style_value(PropertyNameAndID const&) const override; + private: explicit CSSTranslate(JS::Realm&, Is2D, GC::Ref x, GC::Ref y, GC::Ref z); diff --git a/Libraries/LibWeb/CSS/CSSUnitValue.cpp b/Libraries/LibWeb/CSS/CSSUnitValue.cpp index 7e165ed2f8e..0bac75be867 100644 --- a/Libraries/LibWeb/CSS/CSSUnitValue.cpp +++ b/Libraries/LibWeb/CSS/CSSUnitValue.cpp @@ -326,7 +326,7 @@ static Optional create_numeric_value(double value } // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation -WebIDL::ExceptionOr> CSSUnitValue::create_an_internal_representation(PropertyNameAndID const& property) const +WebIDL::ExceptionOr> CSSUnitValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const { // If value is a CSSStyleValue subclass, // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. @@ -338,7 +338,7 @@ WebIDL::ExceptionOr> CSSUnitValue::create_an_int // Return the value. // NB: We store all custom properties as UnresolvedStyleValue, so we always need to create one here. - if (property.is_custom_property()) { + if (perform_type_check == PerformTypeCheck::Yes && property.is_custom_property()) { auto token = [this]() { if (m_unit == "number"_fly_string) return Parser::Token::create_number(Number { Number::Type::Number, m_value }); @@ -361,6 +361,35 @@ WebIDL::ExceptionOr> CSSUnitValue::create_an_int return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unrecognized unit '{}'.", m_unit)) }; } + if (perform_type_check == PerformTypeCheck::No) { + return value->visit( + [&](Number const& number) -> RefPtr { + return NumberStyleValue::create(number.value()); + }, + [&](Percentage const& percentage) -> RefPtr { + return PercentageStyleValue::create(percentage); + }, + [&](Angle const& angle) -> RefPtr { + return AngleStyleValue::create(angle); + }, + [&](Flex const& flex) -> RefPtr { + return FlexStyleValue::create(flex); + }, + [&](Frequency const& frequency) -> RefPtr { + return FrequencyStyleValue::create(frequency); + }, + [&](Length const& length) -> RefPtr { + return LengthStyleValue::create(length); + }, + [&](Resolution const& resolution) -> RefPtr { + return ResolutionStyleValue::create(resolution); + }, + [&](Time const& time) -> RefPtr { + return TimeStyleValue::create(time); + }) + .release_nonnull(); + } + // FIXME: Check types allowed by registered custom properties. auto style_value = value->visit( [&](Number const& number) -> RefPtr { diff --git a/Libraries/LibWeb/CSS/CSSUnitValue.h b/Libraries/LibWeb/CSS/CSSUnitValue.h index 2cf8cf90454..45bc9ec9219 100644 --- a/Libraries/LibWeb/CSS/CSSUnitValue.h +++ b/Libraries/LibWeb/CSS/CSSUnitValue.h @@ -35,7 +35,7 @@ public: virtual bool is_equal_numeric_value(GC::Ref other) const override; virtual Optional create_a_sum_value() const override; - virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&) const override; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override; virtual WebIDL::ExceptionOr> create_calculation_node(CalculationContext const&) const override; private: diff --git a/Libraries/LibWeb/CSS/CSSUnparsedValue.cpp b/Libraries/LibWeb/CSS/CSSUnparsedValue.cpp index 61c95a122a5..1c41e9ffb55 100644 --- a/Libraries/LibWeb/CSS/CSSUnparsedValue.cpp +++ b/Libraries/LibWeb/CSS/CSSUnparsedValue.cpp @@ -168,7 +168,7 @@ WebIDL::ExceptionOr CSSUnparsedValue::to_string() const } // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation -WebIDL::ExceptionOr> CSSUnparsedValue::create_an_internal_representation(PropertyNameAndID const&) const +WebIDL::ExceptionOr> CSSUnparsedValue::create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const { // If value is a CSSStyleValue subclass, // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. diff --git a/Libraries/LibWeb/CSS/CSSUnparsedValue.h b/Libraries/LibWeb/CSS/CSSUnparsedValue.h index 9e5b474eabf..62c27359bbc 100644 --- a/Libraries/LibWeb/CSS/CSSUnparsedValue.h +++ b/Libraries/LibWeb/CSS/CSSUnparsedValue.h @@ -31,7 +31,7 @@ public: virtual WebIDL::ExceptionOr set_value_of_new_indexed_property(u32, JS::Value) override; virtual WebIDL::ExceptionOr to_string() const override; - virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&) const override; + virtual WebIDL::ExceptionOr> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override; private: explicit CSSUnparsedValue(JS::Realm&, Vector); diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 80172421bcc..89ee489a775 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -484,7 +484,8 @@ private: RefPtr parse_rotate_value(TokenStream&); RefPtr parse_stroke_dasharray_value(TokenStream&); RefPtr parse_easing_value(TokenStream&); - RefPtr parse_transform_value(TokenStream&); + RefPtr parse_transform_function_value(TokenStream&); + RefPtr parse_transform_list_value(TokenStream&); RefPtr parse_transform_origin_value(TokenStream&); RefPtr parse_transition_value(TokenStream&); RefPtr parse_transition_property_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp index 82cade4b654..1a7099bd69b 100644 --- a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp @@ -178,6 +178,10 @@ Optional Parser::parse_css_value_for_properties(Readon return parsed.release_value(); if (auto parsed = parse_for_type(ValueType::String); parsed.has_value()) return parsed.release_value(); + if (auto parsed = parse_for_type(ValueType::TransformFunction); parsed.has_value()) + return parsed.release_value(); + if (auto parsed = parse_for_type(ValueType::TransformList); parsed.has_value()) + return parsed.release_value(); if (auto parsed = parse_for_type(ValueType::Url); parsed.has_value()) return parsed.release_value(); @@ -802,10 +806,6 @@ Parser::ParseErrorOr> Parser::parse_css_value(Pr if (auto parsed_value = parse_touch_action_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); return ParseError::SyntaxError; - case PropertyID::Transform: - if (auto parsed_value = parse_transform_value(tokens); parsed_value && !tokens.has_next_token()) - return parsed_value.release_nonnull(); - return ParseError::SyntaxError; case PropertyID::TransformOrigin: if (auto parsed_value = parse_transform_origin_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); @@ -4913,119 +4913,6 @@ RefPtr Parser::parse_touch_action_value(TokenStream Parser::parse_transform_value(TokenStream& tokens) -{ - // = none | - // = + - - if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None)) - return none; - - StyleValueVector transformations; - auto transaction = tokens.begin_transaction(); - while (tokens.has_next_token()) { - auto const& part = tokens.consume_a_token(); - if (!part.is_function()) - return nullptr; - auto maybe_function = transform_function_from_string(part.function().name); - if (!maybe_function.has_value()) - return nullptr; - - auto context_guard = push_temporary_value_parsing_context(FunctionContext { part.function().name }); - - auto function = maybe_function.release_value(); - auto function_metadata = transform_function_metadata(function); - - auto function_tokens = TokenStream { part.function().value }; - auto arguments = parse_a_comma_separated_list_of_component_values(function_tokens); - - if (arguments.size() > function_metadata.parameters.size()) { - ErrorReporter::the().report(InvalidValueError { - .value_type = ""_fly_string, - .value_string = part.function().original_source_text(), - .description = MUST(String::formatted("Too many arguments to {}. max: {}", part.function().name, function_metadata.parameters.size())), - }); - return nullptr; - } - - if (arguments.size() < function_metadata.parameters.size() && function_metadata.parameters[arguments.size()].required) { - ErrorReporter::the().report(InvalidValueError { - .value_type = ""_fly_string, - .value_string = part.function().original_source_text(), - .description = MUST(String::formatted("Required parameter at position {} is missing", arguments.size())), - }); - return nullptr; - } - - StyleValueVector values; - for (auto argument_index = 0u; argument_index < arguments.size(); ++argument_index) { - TokenStream argument_tokens { arguments[argument_index] }; - argument_tokens.discard_whitespace(); - - switch (function_metadata.parameters[argument_index].type) { - case TransformFunctionParameterType::Angle: { - // These are ` | ` in the spec, so we have to check for both kinds. - if (auto angle_value = parse_angle_value(argument_tokens)) { - values.append(angle_value.release_nonnull()); - break; - } - if (argument_tokens.next_token().is(Token::Type::Number) && argument_tokens.next_token().token().number_value() == 0) { - argument_tokens.discard_a_token(); // 0 - values.append(AngleStyleValue::create(Angle::make_degrees(0))); - break; - } - return nullptr; - } - case TransformFunctionParameterType::Length: - case TransformFunctionParameterType::LengthNone: { - if (auto length_value = parse_length_value(argument_tokens)) { - values.append(length_value.release_nonnull()); - break; - } - if (function_metadata.parameters[argument_index].type == TransformFunctionParameterType::LengthNone - && argument_tokens.next_token().is_ident("none"sv)) { - - argument_tokens.discard_a_token(); // none - values.append(KeywordStyleValue::create(Keyword::None)); - break; - } - return nullptr; - } - case TransformFunctionParameterType::LengthPercentage: { - if (auto length_percentage_value = parse_length_percentage_value(argument_tokens)) { - values.append(length_percentage_value.release_nonnull()); - break; - } - return nullptr; - } - case TransformFunctionParameterType::Number: { - if (auto number_value = parse_number_value(argument_tokens)) { - values.append(number_value.release_nonnull()); - break; - } - return nullptr; - } - case TransformFunctionParameterType::NumberPercentage: { - if (auto number_percentage_value = parse_number_percentage_value(argument_tokens)) { - values.append(number_percentage_value.release_nonnull()); - break; - } - return nullptr; - } - } - - argument_tokens.discard_whitespace(); - if (argument_tokens.has_next_token()) - return nullptr; - } - - transformations.append(TransformationStyleValue::create(PropertyID::Transform, function, move(values))); - } - transaction.commit(); - return StyleValueList::create(move(transformations), StyleValueList::Separator::Space); -} - // https://www.w3.org/TR/css-transforms-1/#propdef-transform-origin RefPtr Parser::parse_transform_origin_value(TokenStream& tokens) { diff --git a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp index d690e851347..ebd1df6d31c 100644 --- a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp @@ -67,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -4700,6 +4701,131 @@ NonnullRefPtr Parser::resolve_unresolved_style_value(DOM::Abst return parsed_value.release_value(); } +// https://drafts.csswg.org/css-transforms-1/#typedef-transform-function +RefPtr Parser::parse_transform_function_value(TokenStream& tokens) +{ + auto transaction = tokens.begin_transaction(); + tokens.discard_whitespace(); + auto const& part = tokens.consume_a_token(); + if (!part.is_function()) + return nullptr; + auto maybe_function = transform_function_from_string(part.function().name); + if (!maybe_function.has_value()) + return nullptr; + + auto context_guard = push_temporary_value_parsing_context(FunctionContext { part.function().name }); + + auto function = maybe_function.release_value(); + auto function_metadata = transform_function_metadata(function); + + auto function_tokens = TokenStream { part.function().value }; + auto arguments = parse_a_comma_separated_list_of_component_values(function_tokens); + + if (arguments.size() > function_metadata.parameters.size()) { + ErrorReporter::the().report(InvalidValueError { + .value_type = ""_fly_string, + .value_string = part.function().original_source_text(), + .description = MUST(String::formatted("Too many arguments to {}. max: {}", part.function().name, function_metadata.parameters.size())), + }); + return nullptr; + } + + if (arguments.size() < function_metadata.parameters.size() && function_metadata.parameters[arguments.size()].required) { + ErrorReporter::the().report(InvalidValueError { + .value_type = ""_fly_string, + .value_string = part.function().original_source_text(), + .description = MUST(String::formatted("Required parameter at position {} is missing", arguments.size())), + }); + return nullptr; + } + + StyleValueVector values; + for (auto argument_index = 0u; argument_index < arguments.size(); ++argument_index) { + TokenStream argument_tokens { arguments[argument_index] }; + argument_tokens.discard_whitespace(); + + switch (function_metadata.parameters[argument_index].type) { + case TransformFunctionParameterType::Angle: { + // These are ` | ` in the spec, so we have to check for both kinds. + if (auto angle_value = parse_angle_value(argument_tokens)) { + values.append(angle_value.release_nonnull()); + break; + } + if (argument_tokens.next_token().is(Token::Type::Number) && argument_tokens.next_token().token().number_value() == 0) { + argument_tokens.discard_a_token(); // 0 + values.append(AngleStyleValue::create(Angle::make_degrees(0))); + break; + } + return nullptr; + } + case TransformFunctionParameterType::Length: + case TransformFunctionParameterType::LengthNone: { + if (auto length_value = parse_length_value(argument_tokens)) { + values.append(length_value.release_nonnull()); + break; + } + if (function_metadata.parameters[argument_index].type == TransformFunctionParameterType::LengthNone + && argument_tokens.next_token().is_ident("none"sv)) { + + argument_tokens.discard_a_token(); // none + values.append(KeywordStyleValue::create(Keyword::None)); + break; + } + return nullptr; + } + case TransformFunctionParameterType::LengthPercentage: { + if (auto length_percentage_value = parse_length_percentage_value(argument_tokens)) { + values.append(length_percentage_value.release_nonnull()); + break; + } + return nullptr; + } + case TransformFunctionParameterType::Number: { + if (auto number_value = parse_number_value(argument_tokens)) { + values.append(number_value.release_nonnull()); + break; + } + return nullptr; + } + case TransformFunctionParameterType::NumberPercentage: { + if (auto number_percentage_value = parse_number_percentage_value(argument_tokens)) { + values.append(number_percentage_value.release_nonnull()); + break; + } + return nullptr; + } + } + + argument_tokens.discard_whitespace(); + if (argument_tokens.has_next_token()) + return nullptr; + } + + transaction.commit(); + return TransformationStyleValue::create(PropertyID::Transform, function, move(values)); +} + +// https://drafts.csswg.org/css-transforms-1/#typedef-transform-list +RefPtr Parser::parse_transform_list_value(TokenStream& tokens) +{ + // = + + // https://www.w3.org/TR/css-transforms-1/#transform-property + StyleValueVector transformations; + auto transaction = tokens.begin_transaction(); + while (tokens.has_next_token()) { + if (auto maybe_function = parse_transform_function_value(tokens)) { + transformations.append(maybe_function.release_nonnull()); + tokens.discard_whitespace(); + continue; + } + break; + } + if (transformations.is_empty()) + return {}; + transaction.commit(); + return StyleValueList::create(move(transformations), StyleValueList::Separator::Space); +} + RefPtr Parser::parse_value(ValueType value_type, TokenStream& tokens) { switch (value_type) { @@ -4758,6 +4884,10 @@ RefPtr Parser::parse_value(ValueType value_type, TokenStream -#include #include #include #include diff --git a/Libraries/LibWeb/CSS/StylePropertyMap.cpp b/Libraries/LibWeb/CSS/StylePropertyMap.cpp index 8b2f3450814..56712419376 100644 --- a/Libraries/LibWeb/CSS/StylePropertyMap.cpp +++ b/Libraries/LibWeb/CSS/StylePropertyMap.cpp @@ -62,7 +62,7 @@ static WebIDL::ExceptionOr> create_an_internal_r // To create an internal representation, given a string property and a string or CSSStyleValue value: return value.visit( [&property](GC::Root const& css_style_value) { - return css_style_value->create_an_internal_representation(property); + return css_style_value->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::Yes); }, [&](String const& css_text) -> WebIDL::ExceptionOr> { // If value is a USVString, @@ -71,7 +71,7 @@ static WebIDL::ExceptionOr> create_an_internal_r // FIXME: Avoid passing name as a string, as it gets immediately converted back to PropertyNameAndID. auto result = TRY(CSSStyleValue::parse_a_css_style_value(vm, property.name(), css_text, CSSStyleValue::ParseMultiple::No)); // AD-HOC: Result is a CSSStyleValue but we want an internal representation, so... convert it again I guess? - return result.get>()->create_an_internal_representation(property); + return result.get>()->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::Yes); }); } diff --git a/Libraries/LibWeb/CSS/ValueType.cpp b/Libraries/LibWeb/CSS/ValueType.cpp index 3cc4f6d8507..1759cb764cb 100644 --- a/Libraries/LibWeb/CSS/ValueType.cpp +++ b/Libraries/LibWeb/CSS/ValueType.cpp @@ -61,6 +61,10 @@ Optional value_type_from_string(StringView string) return ValueType::String; if (string.equals_ignoring_ascii_case("time"sv)) return ValueType::Time; + if (string.equals_ignoring_ascii_case("transform-function"sv)) + return ValueType::TransformFunction; + if (string.equals_ignoring_ascii_case("transform-list"sv)) + return ValueType::TransformList; if (string.equals_ignoring_ascii_case("url"sv)) return ValueType::Url; return {}; @@ -123,6 +127,10 @@ StringView value_type_to_string(ValueType value_type) return "String"sv; case Web::CSS::ValueType::Time: return "Time"sv; + case Web::CSS::ValueType::TransformFunction: + return "TransformFunction"sv; + case Web::CSS::ValueType::TransformList: + return "TransformList"sv; case Web::CSS::ValueType::Url: return "Url"sv; } diff --git a/Libraries/LibWeb/CSS/ValueType.h b/Libraries/LibWeb/CSS/ValueType.h index 0286b665b6a..ae352c26dc2 100644 --- a/Libraries/LibWeb/CSS/ValueType.h +++ b/Libraries/LibWeb/CSS/ValueType.h @@ -40,6 +40,8 @@ enum class ValueType : u8 { Resolution, String, Time, + TransformFunction, + TransformList, Url, }; diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index e7be1176602..3b081ec092c 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -3329,11 +3329,6 @@ StringView Document::visibility_state() const VERIFY_NOT_REACHED(); } -void Document::set_visibility_state(Badge, HTML::VisibilityState visibility_state) -{ - m_visibility_state = visibility_state; -} - // https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state void Document::update_the_visibility_state(HTML::VisibilityState visibility_state) { diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 20925cf9878..5e19597a9e2 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -542,9 +542,6 @@ public: // https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state void update_the_visibility_state(HTML::VisibilityState); - // NOTE: This does not fire any events, unlike update_the_visibility_state(). - void set_visibility_state(Badge, HTML::VisibilityState); - void run_the_resize_steps(); void run_the_scroll_steps(); diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index efc08803405..27203e07c8d 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -1245,7 +1245,7 @@ Vector Element::get_client_rects() const Vector rects; if (auto const* paintable_box = this->paintable_box()) { transform = Gfx::extract_2d_affine_transform(paintable_box->transform()); - for (auto const* containing_block = paintable->containing_block(); !containing_block->is_viewport(); containing_block = containing_block->containing_block()) { + for (auto const* containing_block = paintable->containing_block(); !containing_block->is_viewport_paintable(); containing_block = containing_block->containing_block()) { transform = Gfx::extract_2d_affine_transform(containing_block->transform()).multiply(transform); } diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index 086d0cd348d..1aa171c2468 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -788,6 +788,9 @@ void HTMLElement::set_subtree_inertness(bool is_inert) html_element.set_inert(is_inert); return TraversalDecision::Continue; }); + + if (auto paintable_box = this->paintable_box()) + paintable_box->set_needs_paint_only_properties_update(true); } WebIDL::ExceptionOr HTMLElement::cloned(Web::DOM::Node& copy, bool clone_children) const diff --git a/Libraries/LibWeb/HTML/ImageBitmap.h b/Libraries/LibWeb/HTML/ImageBitmap.h index b14d996b9ea..42922f8ac28 100644 --- a/Libraries/LibWeb/HTML/ImageBitmap.h +++ b/Libraries/LibWeb/HTML/ImageBitmap.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,7 @@ struct ImageBitmapOptions { // FIXME: Implement the rest of the fields Optional resize_width; Optional resize_height; + Bindings::ResizeQuality resize_quality = Bindings::ResizeQuality::Low; }; class ImageBitmap final : public Bindings::PlatformObject diff --git a/Libraries/LibWeb/HTML/ImageBitmap.idl b/Libraries/LibWeb/HTML/ImageBitmap.idl index c115c98e940..57324dea0ce 100644 --- a/Libraries/LibWeb/HTML/ImageBitmap.idl +++ b/Libraries/LibWeb/HTML/ImageBitmap.idl @@ -28,5 +28,5 @@ dictionary ImageBitmapOptions { // FIXME: ColorSpaceConversion colorSpaceConversion = "default"; [EnforceRange] unsigned long resizeWidth; [EnforceRange] unsigned long resizeHeight; - // FIXME: ResizeQuality resizeQuality = "low"; + ResizeQuality resizeQuality = "low"; }; diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index 0e74ccdcec2..f1fa19da64d 100644 --- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -12,8 +12,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -198,10 +200,49 @@ static ErrorOr> crop_to_the_source_rectangle_with_for // 6. Let output be the rectangle on the plane denoted by sourceRectangle. auto output = TRY(input->cropped(source_rectangle, Gfx::Color::Transparent)); - // FIXME: 7. Scale output to the size specified by outputWidth and outputHeight. The user agent should use the + // 7. Scale output to the size specified by outputWidth and outputHeight. The user agent should use the // value of the resizeQuality option to guide the choice of scaling algorithm. - (void)output_width; - (void)output_height; + struct ScalingPass { + Gfx::ScalingMode mode { Gfx::ScalingMode::None }; + int width { 0 }; + int height { 0 }; + }; + Vector scaling_passes; + switch (options.has_value() ? options->resize_quality : Bindings::ResizeQuality::Low) { + // NOTE: The spec mentions Bicubic or Lanczos scaling as higher quality options; however, Skia does not implement the latter, so for now we will use SkCubicResampler::Mitchell() for both medium and high + case Bindings::ResizeQuality::High: + // The "high" value indicates a preference for a high level of image interpolation quality. High-quality image interpolation may be more computationally expensive than lower settings. + case Bindings::ResizeQuality::Medium: + // The "medium" value indicates a preference for a medium level of image interpolation quality. + scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BoxSampling, .width = output_width, .height = output_height }); + break; + case Bindings::ResizeQuality::Low: + // The "low" value indicates a preference for a low level of image interpolation quality. Low-quality image interpolation may be more computationally efficient than higher settings. + scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearBlend, .width = output_width, .height = output_height }); + break; + case Bindings::ResizeQuality::Pixelated: { + // The "pixelated" value indicates a preference for scaling the image to preserve the pixelation of the original as much as possible, with minor smoothing as necessary to avoid distorting the image when the target size is not a clean multiple of the original. + + // To implement "pixelated", for each axis independently, first determine the integer multiple of its natural size that puts it closest to the target size and is greater than zero. Scale it to this integer-multiple-size using nearest neighbor, + auto determine_closest_multiple = [](int const source_length, int const output_length) { + // NOTE: The previous cropping action would've failed if the source bitmap we are scaling had invalid lengths. Given the precondition that are lengths are always > 0, this integer division is safe from divide-by-zero and the quotient truncation is also safe as we will always round down + ASSERT(source_length > 0); + return output_length / source_length; + }; + auto const source_width = source_rectangle.width(); + auto const source_height = source_rectangle.height(); + auto const width_multiple = determine_closest_multiple(source_width, output_width); + auto const height_multiple = determine_closest_multiple(source_height, output_height); + if (width_multiple > 0 && height_multiple > 0) + scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::NearestNeighbor, .width = source_width * width_multiple, .height = source_height * height_multiple }); + + // then scale it the rest of the way to the target size using bilinear interpolation. + scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearBlend, .width = output_width, .height = output_height }); + } break; + } + for (ScalingPass& scaling_pass : scaling_passes) { + output = TRY(output->scaled(scaling_pass.width, scaling_pass.height, scaling_pass.mode)); + } // FIXME: 8. If the value of the imageOrientation member of options is "flipY", output must be flipped vertically, // disregarding any image orientation metadata of the source (such as EXIF metadata), if any. [EXIF] diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 90cfa285c7a..48d8258e8e9 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -1293,20 +1293,21 @@ void possibly_update_the_key_generator(GC::Ref store, GC::Ref return; // 2. Let value be the value of key. - auto temp_value = key->value_as_double(); + auto value = key->value_as_double(); // 3. Set value to the minimum of value and 2^53 (9007199254740992). - temp_value = min(temp_value, MAX_KEY_GENERATOR_VALUE); + value = min(value, MAX_KEY_GENERATOR_VALUE); // 4. Set value to the largest integer not greater than value. - u64 value = floor(temp_value); + value = floor(value); // 5. Let generator be store’s key generator. auto& generator = store->key_generator(); // 6. If value is greater than or equal to generator’s current number, then set generator’s current number to value + 1. - if (value >= generator.current_number()) - generator.set(value + 1); + if (value >= static_cast(generator.current_number())) { + generator.set(static_cast(value + 1)); + } } // https://w3c.github.io/IndexedDB/#inject-a-key-into-a-value-using-a-key-path diff --git a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp index 693765f7892..7c4041fb3dc 100644 --- a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp @@ -809,14 +809,18 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain // This monster basically means: "a ListItemBox that does not have specified content in the ::marker pseudo-element". // This happens for ::marker with content 'normal'. // FIXME: We currently so not support ListItemBox-es generated by pseudo-elements. We will need to, eventually. - ListItemBox const* li_box = as_if(box); - bool is_list_item_box_without_css_content = li_box && (!(box.dom_node() && box.dom_node()->is_element() && as_if(box.dom_node())->computed_properties(CSS::PseudoElement::Marker)->property(CSS::PropertyID::Content).is_content())); + auto const* li_box = as_if(box); + auto is_list_item_box_without_css_content = li_box != nullptr; + if (auto const* dom_node = as_if(box.dom_node()); li_box && dom_node) { + if (auto const computed_properties = dom_node->computed_properties(CSS::PseudoElement::Marker)) + is_list_item_box_without_css_content = !computed_properties->property(CSS::PropertyID::Content).is_content(); + } // Before we insert the children of a list item we need to know the location of the marker. // If we do not do this then left-floating elements inside the list item will push the marker to the right, // in some cases even causing it to overlap with the non-floating content of the list. CSSPixels left_space_before_children_formatted; - if (is_list_item_box_without_css_content) { + if (is_list_item_box_without_css_content && li_box->marker()) { // We need to ensure that our height and width are final before we calculate our left offset. // Otherwise, the y at which we calculate the intrusion by floats might be incorrect. ensure_sizes_correct_for_left_offset_calculation(*li_box); diff --git a/Libraries/LibWeb/Layout/FormattingContext.cpp b/Libraries/LibWeb/Layout/FormattingContext.cpp index 0f2ad4eb694..726be189a82 100644 --- a/Libraries/LibWeb/Layout/FormattingContext.cpp +++ b/Libraries/LibWeb/Layout/FormattingContext.cpp @@ -1163,17 +1163,19 @@ void FormattingContext::compute_height_for_absolutely_positioned_non_replaced_el box_state.margin_bottom = margin_bottom.to_px_or_zero(box, width_of_containing_block); } -CSSPixelRect FormattingContext::content_box_rect_in_static_position_ancestor_coordinate_space(Box const& box, Box const& ancestor_box) const +CSSPixelRect FormattingContext::content_box_rect_in_static_position_ancestor_coordinate_space(Box const& box) const { auto box_used_values = m_state.get(box); CSSPixelRect rect = { { 0, 0 }, box_used_values.content_size() }; - for (auto const* current = &box; current; current = current->static_position_containing_block()) { - if (current == &ancestor_box) + VERIFY(box_used_values.offset.is_zero()); // Set as result of this calculation + for (auto const* current = box.static_position_containing_block(); current; current = current->containing_block()) { + if (current == box.containing_block()) return rect; auto const& current_state = m_state.get(*current); rect.translate_by(current_state.offset); } - // If we get here, ancestor_box was not an ancestor of `box`! + // If we get here, `ancestor_box` was not in the containing block chain of the static position containing block of `box`! + // Something about the containing block chain is set up incorrectly then. VERIFY_NOT_REACHED(); } @@ -1245,7 +1247,7 @@ void FormattingContext::layout_absolutely_positioned_element(Box const& box, Ava CSSPixelPoint used_offset; auto static_position = m_state.get(box).static_position(); - auto offset_to_static_parent = content_box_rect_in_static_position_ancestor_coordinate_space(box, *box.containing_block()); + auto offset_to_static_parent = content_box_rect_in_static_position_ancestor_coordinate_space(box); static_position += offset_to_static_parent.location(); if (box.computed_values().inset().top().is_auto() && box.computed_values().inset().bottom().is_auto()) { diff --git a/Libraries/LibWeb/Layout/FormattingContext.h b/Libraries/LibWeb/Layout/FormattingContext.h index e7188b4f7dd..c5b0b91cf24 100644 --- a/Libraries/LibWeb/Layout/FormattingContext.h +++ b/Libraries/LibWeb/Layout/FormattingContext.h @@ -87,7 +87,7 @@ public: [[nodiscard]] CSSPixelRect content_box_rect(LayoutState::UsedValues const&) const; [[nodiscard]] CSSPixelRect content_box_rect_in_ancestor_coordinate_space(LayoutState::UsedValues const&, Box const& ancestor_box) const; [[nodiscard]] CSSPixels box_baseline(Box const&) const; - [[nodiscard]] CSSPixelRect content_box_rect_in_static_position_ancestor_coordinate_space(Box const&, Box const& ancestor_box) const; + [[nodiscard]] CSSPixelRect content_box_rect_in_static_position_ancestor_coordinate_space(Box const&) const; [[nodiscard]] CSSPixels containing_block_width_for(NodeWithStyleAndBoxModelMetrics const&) const; diff --git a/Libraries/LibWeb/Painting/BackgroundPainting.cpp b/Libraries/LibWeb/Painting/BackgroundPainting.cpp index 9c5738c61d6..dd3a0593136 100644 --- a/Libraries/LibWeb/Painting/BackgroundPainting.cpp +++ b/Libraries/LibWeb/Painting/BackgroundPainting.cpp @@ -167,7 +167,7 @@ void paint_background(DisplayListRecordingContext& context, PaintableBox const& background_positioning_area.set_location(paintable_box.layout_node().root().navigable()->viewport_scroll_offset()); break; case CSS::BackgroundAttachment::Local: - if (!paintable_box.is_viewport()) { + if (!paintable_box.is_viewport_paintable()) { auto scroll_offset = paintable_box.scroll_offset(); background_positioning_area.translate_by(-scroll_offset.x(), -scroll_offset.y()); } diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index 33f1286329e..07bdaea7f4a 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -52,6 +52,17 @@ String Paintable::debug_description() const return MUST(String::formatted("{}({})", class_name(), layout_node().debug_description())); } +void Paintable::resolve_paint_properties() +{ + m_visible_for_hit_testing = true; + if (auto dom_node = this->dom_node(); dom_node && dom_node->is_inert()) { + // https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees + // When a node is inert: + // - Hit-testing must act as if the 'pointer-events' CSS property were set to 'none'. + m_visible_for_hit_testing = false; + } +} + bool Paintable::is_visible() const { auto const& computed_values = this->computed_values(); @@ -90,13 +101,7 @@ CSS::ImmutableComputedValues const& Paintable::computed_values() const bool Paintable::visible_for_hit_testing() const { - // https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees - // When a node is inert: - // - Hit-testing must act as if the 'pointer-events' CSS property were set to 'none'. - if (auto dom_node = this->dom_node(); dom_node && dom_node->is_inert()) - return false; - - return computed_values().pointer_events() != CSS::PointerEvents::None; + return m_visible_for_hit_testing && computed_values().pointer_events() != CSS::PointerEvents::None; } void Paintable::set_dom_node(GC::Ptr dom_node) diff --git a/Libraries/LibWeb/Painting/Paintable.h b/Libraries/LibWeb/Painting/Paintable.h index ba280ad69bd..6689f90005c 100644 --- a/Libraries/LibWeb/Painting/Paintable.h +++ b/Libraries/LibWeb/Painting/Paintable.h @@ -145,7 +145,7 @@ public: SelectionState selection_state() const { return m_selection_state; } void set_selection_state(SelectionState state) { m_selection_state = state; } - virtual void resolve_paint_properties() { } + virtual void resolve_paint_properties(); [[nodiscard]] String debug_description() const; @@ -177,6 +177,7 @@ private: bool m_absolutely_positioned : 1 { false }; bool m_floating : 1 { false }; bool m_inline : 1 { false }; + bool m_visible_for_hit_testing : 1 { true }; bool m_needs_paint_only_properties_update : 1 { true }; }; diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index 27f720b08b9..79da8357f75 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -95,7 +95,7 @@ PaintableWithLines::~PaintableWithLines() CSSPixelPoint PaintableBox::scroll_offset() const { - if (is_viewport()) { + if (is_viewport_paintable()) { auto navigable = document().navigable(); VERIFY(navigable); return navigable->viewport_scroll_offset(); @@ -361,7 +361,7 @@ bool PaintableBox::could_be_scrolled_by_wheel_event(ScrollDirection direction) c auto scrollable_overflow_size = direction == ScrollDirection::Horizontal ? scrollable_overflow_rect->width() : scrollable_overflow_rect->height(); auto scrollport_size = direction == ScrollDirection::Horizontal ? absolute_padding_box_rect().width() : absolute_padding_box_rect().height(); auto overflow_value_allows_scrolling = overflow == CSS::Overflow::Auto || overflow == CSS::Overflow::Scroll; - if ((is_viewport() && overflow != CSS::Overflow::Hidden) || overflow_value_allows_scrolling) + if ((is_viewport_paintable() && overflow != CSS::Overflow::Hidden) || overflow_value_allows_scrolling) return scrollable_overflow_size > scrollport_size; return false; } @@ -488,7 +488,7 @@ void PaintableBox::paint(DisplayListRecordingContext& context, PaintPhase phase) } } - if (phase == PaintPhase::Overlay && (g_paint_viewport_scrollbars || !is_viewport()) && computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) { + if (phase == PaintPhase::Overlay && (g_paint_viewport_scrollbars || !is_viewport_paintable()) && computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) { auto scrollbar_colors = computed_values().scrollbar_color(); if (auto scrollbar_data = compute_scrollbar_data(ScrollDirection::Vertical); scrollbar_data.has_value()) { auto gutter_rect = context.rounded_device_rect(scrollbar_data->gutter_rect).to_type(); @@ -1153,10 +1153,10 @@ void PaintableBox::scroll_to_mouse_position(CSSPixelPoint position) auto scroll_position_in_pixels = CSSPixels::nearest_value_for(scroll_position * (scrollable_overflow_size - padding_size)); // Set the new scroll offset. - auto new_scroll_offset = is_viewport() ? document().navigable()->viewport_scroll_offset() : scroll_offset(); + auto new_scroll_offset = is_viewport_paintable() ? document().navigable()->viewport_scroll_offset() : scroll_offset(); new_scroll_offset.set_primary_offset_for_orientation(orientation, scroll_position_in_pixels); - if (is_viewport()) + if (is_viewport_paintable()) document().navigable()->perform_scroll_of_viewport(new_scroll_offset); else (void)set_scroll_offset(new_scroll_offset); @@ -1213,7 +1213,7 @@ TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType typ if (hit_test_scrollbars(position, callback) == TraversalDecision::Break) return TraversalDecision::Break; - if (is_viewport()) { + if (is_viewport_paintable()) { auto& viewport_paintable = const_cast(static_cast(*this)); viewport_paintable.build_stacking_context_tree_if_needed(); viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed(); @@ -1279,7 +1279,7 @@ Optional PaintableBox::hit_test(CSSPixelPoint position, HitTestTy TraversalDecision PaintableBox::hit_test_children(CSSPixelPoint position, HitTestType type, Function const& callback) const { for (auto const* child = last_child(); child; child = child->previous_sibling()) { - if (child->layout_node().is_positioned() && child->computed_values().z_index().value_or(0) == 0) + if (child->is_positioned() && child->computed_values().z_index().value_or(0) == 0) continue; if (child->has_stacking_context()) continue; @@ -1390,7 +1390,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy } } - if (!stacking_context() && is_visible() && (!layout_node().is_anonymous() || layout_node().is_positioned()) + if (!stacking_context() && is_visible() && (!layout_node().is_anonymous() || is_positioned()) && absolute_border_box_rect().contains(offset_position_adjusted_by_scroll_offset)) { if (callback(HitTestResult { const_cast(*this) }) == TraversalDecision::Break) return TraversalDecision::Break; diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index a9978b14d88..3f321249d02 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -213,8 +213,6 @@ public: Optional get_clip_rect() const; - bool is_viewport() const { return layout_node_with_style_and_box_metrics().is_viewport(); } - virtual bool wants_mouse_events() const override; CSSPixelRect transform_box_rect() const; diff --git a/Libraries/LibWeb/Painting/SVGPathPaintable.cpp b/Libraries/LibWeb/Painting/SVGPathPaintable.cpp index 9db90ef35e1..113b76f7c11 100644 --- a/Libraries/LibWeb/Painting/SVGPathPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGPathPaintable.cpp @@ -24,11 +24,6 @@ SVGPathPaintable::SVGPathPaintable(Layout::SVGGraphicsBox const& layout_box) { } -Layout::SVGGraphicsBox const& SVGPathPaintable::layout_box() const -{ - return static_cast(layout_node()); -} - TraversalDecision SVGPathPaintable::hit_test(CSSPixelPoint position, HitTestType type, Function const& callback) const { if (!computed_path().has_value()) @@ -55,8 +50,7 @@ void SVGPathPaintable::resolve_paint_properties() { Base::resolve_paint_properties(); - auto& graphics_element = layout_box().dom_node(); - + auto& graphics_element = dom_node(); m_stroke_thickness = graphics_element.stroke_width().value_or(1); m_stroke_dasharray = graphics_element.stroke_dasharray(); m_stroke_dashoffset = graphics_element.stroke_dashoffset().value_or(0); @@ -72,7 +66,7 @@ void SVGPathPaintable::paint(DisplayListRecordingContext& context, PaintPhase ph if (phase != PaintPhase::Foreground) return; - auto& graphics_element = layout_box().dom_node(); + auto& graphics_element = dom_node(); auto const* svg_node = layout_box().first_ancestor_of_type(); auto svg_element_rect = svg_node->paintable_box()->absolute_rect(); diff --git a/Libraries/LibWeb/Painting/SVGPathPaintable.h b/Libraries/LibWeb/Painting/SVGPathPaintable.h index 4678632e3e1..6ed4793136d 100644 --- a/Libraries/LibWeb/Painting/SVGPathPaintable.h +++ b/Libraries/LibWeb/Painting/SVGPathPaintable.h @@ -24,7 +24,7 @@ public: virtual void paint(DisplayListRecordingContext&, PaintPhase) const override; - Layout::SVGGraphicsBox const& layout_box() const; + SVG::SVGGraphicsElement const& dom_node() const { return as(*Paintable::dom_node()); } void set_computed_path(Gfx::Path path) { diff --git a/Libraries/LibWeb/Painting/StackingContext.cpp b/Libraries/LibWeb/Painting/StackingContext.cpp index 0a2590e735c..807581f5023 100644 --- a/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Libraries/LibWeb/Painting/StackingContext.cpp @@ -86,7 +86,7 @@ static PaintPhase to_paint_phase(StackingContext::StackingContextPaintPhase phas void StackingContext::paint_node_as_stacking_context(Paintable const& paintable, DisplayListRecordingContext& context) { - if (paintable.layout_node().is_svg_svg_box()) { + if (paintable.is_svg_svg_paintable()) { paint_svg(context, static_cast(paintable), PaintPhase::Foreground); return; } @@ -121,7 +121,7 @@ void StackingContext::paint_descendants(DisplayListRecordingContext& context, Pa if (child.has_stacking_context()) return IterationDecision::Continue; - if (child.layout_node().is_svg_svg_box()) { + if (child.is_svg_svg_paintable()) { paint_svg(context, static_cast(child), to_paint_phase(phase)); return IterationDecision::Continue; } @@ -198,15 +198,15 @@ void StackingContext::paint_descendants(DisplayListRecordingContext& context, Pa void StackingContext::paint_child(DisplayListRecordingContext& context, StackingContext const& child) { - VERIFY(!child.paintable_box().layout_node().is_svg_box()); + VERIFY(!child.paintable_box().is_svg_paintable()); const_cast(child).set_last_paint_generation_id(context.paint_generation_id()); child.paint(context); } void StackingContext::paint_internal(DisplayListRecordingContext& context) const { - VERIFY(!paintable_box().layout_node().is_svg_box()); - if (paintable_box().layout_node().is_svg_svg_box()) { + VERIFY(!paintable_box().is_svg_paintable()); + if (paintable_box().is_svg_svg_paintable()) { auto const& svg_svg_paintable = static_cast(paintable_box()); paint_node(svg_svg_paintable, context, PaintPhase::Background); paint_node(svg_svg_paintable, context, PaintPhase::Border); diff --git a/Libraries/LibWeb/Painting/ViewportPaintable.cpp b/Libraries/LibWeb/Painting/ViewportPaintable.cpp index a6f7a47659c..f400803b946 100644 --- a/Libraries/LibWeb/Painting/ViewportPaintable.cpp +++ b/Libraries/LibWeb/Painting/ViewportPaintable.cpp @@ -139,7 +139,7 @@ void ViewportPaintable::assign_clip_frames() paintable_box.set_own_clip_frame(clip_frame.value()); } } - for (auto block = paintable.containing_block(); !block->is_viewport(); block = block->containing_block()) { + for (auto block = paintable.containing_block(); !block->is_viewport_paintable(); block = block->containing_block()) { if (auto clip_frame = clip_state.get(block); clip_frame.has_value()) { if (paintable.is_paintable_box()) { auto& paintable_box = static_cast(paintable); diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index da005ddbb82..525efcb9247 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -17,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -117,6 +117,7 @@ ErrorOr Application::initialize(Main::Arguments const& arguments) bool disable_site_isolation = false; bool enable_idl_tracing = false; bool disable_http_cache = false; + bool enable_http_disk_cache = false; bool enable_autoplay = false; bool expose_internals_object = false; bool force_cpu_painting = false; @@ -164,6 +165,7 @@ ErrorOr Application::initialize(Main::Arguments const& arguments) args_parser.add_option(disable_site_isolation, "Disable site isolation", "disable-site-isolation"); args_parser.add_option(enable_idl_tracing, "Enable IDL tracing", "enable-idl-tracing"); args_parser.add_option(disable_http_cache, "Disable HTTP cache", "disable-http-cache"); + args_parser.add_option(enable_http_disk_cache, "Enable HTTP disk cache", "enable-http-disk-cache"); args_parser.add_option(enable_autoplay, "Enable multimedia autoplay", "enable-autoplay"); args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object"); args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting"); @@ -228,7 +230,6 @@ ErrorOr Application::initialize(Main::Arguments const& arguments) .urls = sanitize_urls(raw_urls, m_settings.new_tab_page_url()), .raw_urls = move(raw_urls), .headless_mode = headless_mode, - .certificates = move(certificates), .new_window = new_window ? NewWindow::Yes : NewWindow::No, .force_new_process = force_new_process ? ForceNewProcess::Yes : ForceNewProcess::No, .allow_popups = allow_popups ? AllowPopups::Yes : AllowPopups::No, @@ -252,6 +253,11 @@ ErrorOr Application::initialize(Main::Arguments const& arguments) if (webdriver_content_ipc_path.has_value()) m_browser_options.webdriver_content_ipc_path = *webdriver_content_ipc_path; + m_request_server_options = { + .certificates = move(certificates), + .enable_http_disk_cache = enable_http_disk_cache ? EnableHTTPDiskCache::Yes : EnableHTTPDiskCache::No, + }; + m_web_content_options = { .command_line = MUST(String::join(' ', m_arguments.strings)), .executable_path = MUST(String::from_byte_string(MUST(Core::System::current_executable_path()))), @@ -347,9 +353,12 @@ ErrorOr Application::launch_services() }; if (m_browser_options.disable_sql_database == DisableSQLDatabase::No) { - m_database = Database::create().release_value_but_fixme_should_propagate_errors(); - m_cookie_jar = CookieJar::create(*m_database).release_value_but_fixme_should_propagate_errors(); - m_storage_jar = StorageJar::create(*m_database).release_value_but_fixme_should_propagate_errors(); + // FIXME: Move this to a generic "Ladybird data directory" helper. + auto database_path = ByteString::formatted("{}/Ladybird", Core::StandardPaths::user_data_directory()); + + m_database = TRY(Database::Database::create(database_path, "Ladybird"sv)); + m_cookie_jar = TRY(CookieJar::create(*m_database)); + m_storage_jar = TRY(StorageJar::create(*m_database)); } else { m_cookie_jar = CookieJar::create(); m_storage_jar = StorageJar::create(); @@ -815,7 +824,10 @@ void Application::initialize_actions() m_debug_menu->add_separator(); m_debug_menu->add_action(Action::create("Collect Garbage"sv, ActionID::CollectGarbage, debug_request("collect-garbage"sv))); - m_debug_menu->add_action(Action::create("Clear Cache"sv, ActionID::ClearCache, debug_request("clear-cache"sv))); + m_debug_menu->add_action(Action::create("Clear Cache"sv, ActionID::ClearCache, [this, clear_memory_cache = debug_request("clear_cache")]() { + m_request_server_client->async_clear_cache(); + clear_memory_cache(); + })); m_debug_menu->add_action(Action::create("Clear All Cookies"sv, ActionID::ClearCookies, [this]() { m_cookie_jar->clear_all_cookies(); })); m_debug_menu->add_separator(); diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index d0a13269dfa..424615c5260 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ public: static Settings& settings() { return the().m_settings; } static BrowserOptions const& browser_options() { return the().m_browser_options; } + static RequestServerOptions const& request_server_options() { return the().m_request_server_options; } static WebContentOptions& web_content_options() { return the().m_web_content_options; } static Requests::RequestClient& request_server_client() { return *the().m_request_server_client; } @@ -173,6 +175,7 @@ private: Main::Arguments m_arguments; BrowserOptions m_browser_options; + RequestServerOptions m_request_server_options; WebContentOptions m_web_content_options; RefPtr m_request_server_client; @@ -181,7 +184,7 @@ private: RefPtr m_spare_web_content_process; bool m_has_queued_task_to_launch_spare_web_content_process { false }; - RefPtr m_database; + RefPtr m_database; OwnPtr m_cookie_jar; OwnPtr m_storage_jar; diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt index 532347e2f47..9503d8f6cd1 100644 --- a/Libraries/LibWebView/CMakeLists.txt +++ b/Libraries/LibWebView/CMakeLists.txt @@ -7,7 +7,6 @@ set(SOURCES BrowserProcess.cpp ConsoleOutput.cpp CookieJar.cpp - Database.cpp DOMNodeProperties.cpp HeadlessWebView.cpp HelperProcess.cpp @@ -70,16 +69,13 @@ set(GENERATED_SOURCES ) ladybird_lib(LibWebView webview EXPLICIT_SYMBOL_EXPORT) -target_link_libraries(LibWebView PRIVATE LibCore LibDevTools LibFileSystem LibGfx LibImageDecoderClient LibIPC LibRequests LibJS LibWeb LibUnicode LibURL LibSyntax LibTextCodec) +target_link_libraries(LibWebView PRIVATE LibCore LibDatabase LibDevTools LibFileSystem LibGfx LibImageDecoderClient LibIPC LibRequests LibJS LibWeb LibUnicode LibURL LibSyntax LibTextCodec) if (APPLE) target_link_libraries(LibWebView PRIVATE LibThreading) endif() # Third-party -find_package(SQLite3 REQUIRED) -target_link_libraries(LibWebView PRIVATE SQLite::SQLite3) - if (HAS_FONTCONFIG) target_link_libraries(LibWebView PRIVATE Fontconfig::Fontconfig) endif() diff --git a/Libraries/LibWebView/CookieJar.cpp b/Libraries/LibWebView/CookieJar.cpp index 7b7e4bebc0f..3972273586e 100644 --- a/Libraries/LibWebView/CookieJar.cpp +++ b/Libraries/LibWebView/CookieJar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,7 +21,7 @@ namespace WebView { static constexpr auto DATABASE_SYNCHRONIZATION_TIMER = AK::Duration::from_seconds(30); -ErrorOr> CookieJar::create(Database& database) +ErrorOr> CookieJar::create(Database::Database& database) { Statements statements {}; @@ -665,7 +666,7 @@ void CookieJar::PersistedStorage::insert_cookie(Web::Cookie::Cookie const& cooki cookie.persistent); } -static Web::Cookie::Cookie parse_cookie(Database& database, Database::StatementID statement_id) +static Web::Cookie::Cookie parse_cookie(Database::Database& database, Database::StatementID statement_id) { int column = 0; auto convert_text = [&](auto& field) { field = database.result_column(statement_id, column++); }; diff --git a/Libraries/LibWebView/CookieJar.h b/Libraries/LibWebView/CookieJar.h index 6745b132b5e..2aa79c3c346 100644 --- a/Libraries/LibWebView/CookieJar.h +++ b/Libraries/LibWebView/CookieJar.h @@ -13,10 +13,10 @@ #include #include #include +#include #include #include #include -#include #include namespace WebView { @@ -76,13 +76,13 @@ class WEBVIEW_API CookieJar { void insert_cookie(Web::Cookie::Cookie const& cookie); TransientStorage::Cookies select_all_cookies(); - Database& database; + Database::Database& database; Statements statements; RefPtr synchronization_timer {}; }; public: - static ErrorOr> create(Database&); + static ErrorOr> create(Database::Database&); static NonnullOwnPtr create(); ~CookieJar(); diff --git a/Libraries/LibWebView/Forward.h b/Libraries/LibWebView/Forward.h index 1e5f7bad587..4507d42117a 100644 --- a/Libraries/LibWebView/Forward.h +++ b/Libraries/LibWebView/Forward.h @@ -16,7 +16,6 @@ class Action; class Application; class Autocomplete; class CookieJar; -class Database; class Menu; class OutOfProcessWebView; class ProcessManager; diff --git a/Libraries/LibWebView/HelperProcess.cpp b/Libraries/LibWebView/HelperProcess.cpp index af7c29d15d1..b97d0527f96 100644 --- a/Libraries/LibWebView/HelperProcess.cpp +++ b/Libraries/LibWebView/HelperProcess.cpp @@ -201,17 +201,23 @@ ErrorOr> launch_web_worker_process(Web ErrorOr> launch_request_server_process() { + auto const& request_server_options = Application::request_server_options(); + Vector arguments; - for (auto const& certificate : WebView::Application::browser_options().certificates) + for (auto const& certificate : request_server_options.certificates) arguments.append(ByteString::formatted("--certificate={}", certificate)); + if (request_server_options.enable_http_disk_cache == EnableHTTPDiskCache::Yes) + arguments.append("--enable-http-disk-cache"sv); + if (auto server = mach_server_name(); server.has_value()) { arguments.append("--mach-server-name"sv); arguments.append(server.value()); } auto client = TRY(launch_server_process("RequestServer"sv, move(arguments))); + WebView::Application::settings().dns_settings().visit( [](WebView::SystemDNS) {}, [&](WebView::DNSOverTLS const& dns_over_tls) { diff --git a/Libraries/LibWebView/Options.h b/Libraries/LibWebView/Options.h index 6380faa84e5..b4decc00bef 100644 --- a/Libraries/LibWebView/Options.h +++ b/Libraries/LibWebView/Options.h @@ -74,7 +74,6 @@ struct BrowserOptions { Optional headless_mode; int window_width { 800 }; int window_height { 600 }; - Vector certificates {}; NewWindow new_window { NewWindow::No }; ForceNewProcess force_new_process { ForceNewProcess::No }; AllowPopups allow_popups { AllowPopups::No }; @@ -87,6 +86,16 @@ struct BrowserOptions { Optional devtools_port; }; +enum class EnableHTTPDiskCache { + No, + Yes, +}; + +struct RequestServerOptions { + Vector certificates; + EnableHTTPDiskCache enable_http_disk_cache { EnableHTTPDiskCache::No }; +}; + enum class IsLayoutTestMode { No, Yes, diff --git a/Libraries/LibWebView/StorageJar.cpp b/Libraries/LibWebView/StorageJar.cpp index c11194b0882..e0b22e0672e 100644 --- a/Libraries/LibWebView/StorageJar.cpp +++ b/Libraries/LibWebView/StorageJar.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace WebView { @@ -13,7 +14,7 @@ namespace WebView { // Quota size is specified in https://storage.spec.whatwg.org/#registered-storage-endpoints static constexpr size_t LOCAL_STORAGE_QUOTA = 5 * MiB; -ErrorOr> StorageJar::create(Database& database) +ErrorOr> StorageJar::create(Database::Database& database) { Statements statements {}; diff --git a/Libraries/LibWebView/StorageJar.h b/Libraries/LibWebView/StorageJar.h index 39b835ecd5a..0baae08f772 100644 --- a/Libraries/LibWebView/StorageJar.h +++ b/Libraries/LibWebView/StorageJar.h @@ -9,8 +9,8 @@ #include #include #include +#include #include -#include #include #include @@ -31,7 +31,7 @@ class WEBVIEW_API StorageJar { AK_MAKE_NONMOVABLE(StorageJar); public: - static ErrorOr> create(Database&); + static ErrorOr> create(Database::Database&); static NonnullOwnPtr create(); ~StorageJar(); @@ -71,7 +71,7 @@ private: void clear(StorageEndpointType storage_endpoint, String const& storage_key); Vector get_keys(StorageEndpointType storage_endpoint, String const& storage_key); - Database& database; + Database::Database& database; Statements statements; }; diff --git a/Meta/import-wpt-test.py b/Meta/import-wpt-test.py index 496e60bda7d..669c39e4542 100755 --- a/Meta/import-wpt-test.py +++ b/Meta/import-wpt-test.py @@ -157,6 +157,10 @@ def map_to_path( if source.resource.startswith("/") or not is_resource: file_path = Path(base_directory, source.resource.lstrip("/")) else: + parsed_url = urlparse(source.resource) + if parsed_url.scheme != "": + print(f"Skipping '{source.resource}'. Downloading external resources is not supported.") + continue # Add it as a sibling path if it's a relative resource sibling_location = Path(resource_path).parent parent_directory = Path(base_directory, sibling_location) diff --git a/Services/RequestServer/CMakeLists.txt b/Services/RequestServer/CMakeLists.txt index 59d862fed7d..dc23582c626 100644 --- a/Services/RequestServer/CMakeLists.txt +++ b/Services/RequestServer/CMakeLists.txt @@ -3,6 +3,10 @@ set(CMAKE_AUTORCC OFF) set(CMAKE_AUTOUIC OFF) set(SOURCES + Cache/CacheEntry.cpp + Cache/CacheIndex.cpp + Cache/DiskCache.cpp + Cache/Utilities.cpp ConnectionFromClient.cpp WebSocketImplCurl.cpp ) @@ -33,7 +37,7 @@ target_include_directories(requestserverservice PRIVATE ${CMAKE_CURRENT_BINARY_D target_include_directories(requestserverservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/) target_link_libraries(RequestServer PRIVATE requestserverservice) -target_link_libraries(requestserverservice PUBLIC LibCore LibDNS LibMain LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl) +target_link_libraries(requestserverservice PUBLIC LibCore LibDatabase LibDNS LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl) target_link_libraries(requestserverservice PRIVATE OpenSSL::Crypto OpenSSL::SSL) if (${CMAKE_SYSTEM_NAME} MATCHES "SunOS") diff --git a/Services/RequestServer/Cache/CacheEntry.cpp b/Services/RequestServer/Cache/CacheEntry.cpp new file mode 100644 index 00000000000..b975af67e94 --- /dev/null +++ b/Services/RequestServer/Cache/CacheEntry.cpp @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RequestServer { + +static LexicalPath path_for_cache_key(LexicalPath const& cache_directory, u64 cache_key) +{ + return cache_directory.append(MUST(String::formatted("{:016x}", cache_key))); +} + +ErrorOr CacheHeader::read_from_stream(Stream& stream) +{ + CacheHeader header; + header.magic = TRY(stream.read_value()); + header.version = TRY(stream.read_value()); + header.url_size = TRY(stream.read_value()); + header.url_hash = TRY(stream.read_value()); + header.status_code = TRY(stream.read_value()); + header.reason_phrase_size = TRY(stream.read_value()); + header.reason_phrase_hash = TRY(stream.read_value()); + header.headers_size = TRY(stream.read_value()); + header.headers_hash = TRY(stream.read_value()); + return header; +} + +ErrorOr CacheHeader::write_to_stream(Stream& stream) const +{ + TRY(stream.write_value(magic)); + TRY(stream.write_value(version)); + TRY(stream.write_value(url_size)); + TRY(stream.write_value(url_hash)); + TRY(stream.write_value(status_code)); + TRY(stream.write_value(reason_phrase_size)); + TRY(stream.write_value(reason_phrase_hash)); + TRY(stream.write_value(headers_size)); + TRY(stream.write_value(headers_hash)); + return {}; +} + +ErrorOr CacheFooter::write_to_stream(Stream& stream) const +{ + TRY(stream.write_value(data_size)); + TRY(stream.write_value(crc32)); + return {}; +} + +ErrorOr CacheFooter::read_from_stream(Stream& stream) +{ + CacheFooter footer; + footer.data_size = TRY(stream.read_value()); + footer.crc32 = TRY(stream.read_value()); + return footer; +} + +CacheEntry::CacheEntry(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, CacheHeader cache_header) + : m_disk_cache(disk_cache) + , m_index(index) + , m_cache_key(cache_key) + , m_url(move(url)) + , m_path(move(path)) + , m_cache_header(cache_header) +{ +} + +void CacheEntry::remove() +{ + (void)FileSystem::remove(m_path.string(), FileSystem::RecursionMode::Disallowed); + m_index.remove_entry(m_cache_key); +} + +void CacheEntry::close_and_destory_cache_entry() +{ + m_disk_cache.cache_entry_closed({}, *this); +} + +ErrorOr> CacheEntryWriter::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, u32 status_code, Optional reason_phrase, HTTP::HeaderMap const& headers, UnixDateTime request_time) +{ + auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key); + + auto unbuffered_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write)); + auto file = TRY(Core::OutputBufferedFile::create(move(unbuffered_file))); + + CacheHeader cache_header; + + auto result = [&]() -> ErrorOr { + StringBuilder builder; + auto headers_serializer = TRY(JsonArraySerializer<>::try_create(builder)); + + for (auto const& header : headers.headers()) { + if (is_header_exempted_from_storage(header.name)) + continue; + + auto header_serializer = TRY(headers_serializer.add_object()); + TRY(header_serializer.add("name"sv, header.name)); + TRY(header_serializer.add("value"sv, header.value)); + TRY(header_serializer.finish()); + } + + TRY(headers_serializer.finish()); + + cache_header.url_size = url.byte_count(); + cache_header.url_hash = url.hash(); + + cache_header.status_code = status_code; + cache_header.reason_phrase_size = reason_phrase.has_value() ? reason_phrase->byte_count() : 0; + cache_header.reason_phrase_hash = reason_phrase.has_value() ? reason_phrase->hash() : 0; + + auto serialized_headers = builder.string_view(); + cache_header.headers_size = serialized_headers.length(); + cache_header.headers_hash = serialized_headers.hash(); + + TRY(file->write_value(cache_header)); + TRY(file->write_until_depleted(url)); + if (reason_phrase.has_value()) + TRY(file->write_until_depleted(*reason_phrase)); + TRY(file->write_until_depleted(serialized_headers)); + + return {}; + }(); + + if (result.is_error()) { + (void)FileSystem::remove(path.string(), FileSystem::RecursionMode::Disallowed); + return result.release_error(); + } + + return adopt_own(*new CacheEntryWriter { disk_cache, index, cache_key, move(url), path, move(file), cache_header, request_time }); +} + +CacheEntryWriter::CacheEntryWriter(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr file, CacheHeader cache_header, UnixDateTime request_time) + : CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header) + , m_file(move(file)) + , m_request_time(request_time) + , m_response_time(UnixDateTime::now()) +{ +} + +ErrorOr CacheEntryWriter::write_data(ReadonlyBytes data) +{ + if (m_marked_for_deletion) { + close_and_destory_cache_entry(); + return Error::from_string_literal("Cache entry has been deleted"); + } + + if (auto result = m_file->write_until_depleted(data); result.is_error()) { + dbgln("\033[31;1mUnable to write to cache entry for{}\033[0m {}: {}", m_url, result.error()); + + remove(); + close_and_destory_cache_entry(); + + return result.release_error(); + } + + m_cache_footer.data_size += data.size(); + + // FIXME: Update the crc. + + dbgln("\033[36;1mSaved {} bytes for\033[0m {}", data.size(), m_url); + return {}; +} + +ErrorOr CacheEntryWriter::flush() +{ + ScopeGuard guard { [&]() { close_and_destory_cache_entry(); } }; + + if (m_marked_for_deletion) + return Error::from_string_literal("Cache entry has been deleted"); + + if (auto result = m_file->write_value(m_cache_footer); result.is_error()) { + dbgln("\033[31;1mUnable to flush cache entry for{}\033[0m {}: {}", m_url, result.error()); + remove(); + + return result.release_error(); + } + + m_index.create_entry(m_cache_key, m_url, m_cache_footer.data_size, m_request_time, m_response_time); + + dbgln("\033[34;1mFinished caching\033[0m {} ({} bytes)", m_url, m_cache_footer.data_size); + return {}; +} + +ErrorOr> CacheEntryReader::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, u64 data_size) +{ + auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key); + + auto file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Read)); + auto fd = file->fd(); + + CacheHeader cache_header; + + String url; + Optional reason_phrase; + HTTP::HeaderMap headers; + + auto result = [&]() -> ErrorOr { + cache_header = TRY(file->read_value()); + + if (cache_header.magic != CacheHeader::CACHE_MAGIC) + return Error::from_string_literal("Magic value mismatch"); + if (cache_header.version != CacheHeader::CACHE_VERSION) + return Error::from_string_literal("Version mismatch"); + + url = TRY(String::from_stream(*file, cache_header.url_size)); + if (url.hash() != cache_header.url_hash) + return Error::from_string_literal("URL hash mismatch"); + + if (cache_header.reason_phrase_size != 0) { + reason_phrase = TRY(String::from_stream(*file, cache_header.reason_phrase_size)); + if (reason_phrase->hash() != cache_header.reason_phrase_hash) + return Error::from_string_literal("Reason phrase hash mismatch"); + } + + auto serialized_headers = TRY(String::from_stream(*file, cache_header.headers_size)); + if (serialized_headers.hash() != cache_header.headers_hash) + return Error::from_string_literal("HTTP headers hash mismatch"); + + auto json_headers = TRY(JsonValue::from_string(serialized_headers)); + if (!json_headers.is_array()) + return Error::from_string_literal("Expected HTTP headers to be a JSON array"); + + TRY(json_headers.as_array().try_for_each([&](JsonValue const& header) -> ErrorOr { + if (!header.is_object()) + return Error::from_string_literal("Expected headers entry to be a JSON object"); + + auto name = header.as_object().get_string("name"sv); + auto value = header.as_object().get_string("value"sv); + + if (!name.has_value() || !value.has_value()) + return Error::from_string_literal("Missing/invalid data in headers entry"); + + headers.set(name->to_byte_string(), value->to_byte_string()); + return {}; + })); + + return {}; + }(); + + if (result.is_error()) { + (void)FileSystem::remove(path.string(), FileSystem::RecursionMode::Disallowed); + return result.release_error(); + } + + auto data_offset = sizeof(CacheHeader) + cache_header.url_size + cache_header.reason_phrase_size + cache_header.headers_size; + + return adopt_own(*new CacheEntryReader { disk_cache, index, cache_key, move(url), move(path), move(file), fd, cache_header, move(reason_phrase), move(headers), data_offset, data_size }); +} + +CacheEntryReader::CacheEntryReader(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr file, int fd, CacheHeader cache_header, Optional reason_phrase, HTTP::HeaderMap header_map, u64 data_offset, u64 data_size) + : CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header) + , m_file(move(file)) + , m_fd(fd) + , m_reason_phrase(move(reason_phrase)) + , m_headers(move(header_map)) + , m_data_offset(data_offset) + , m_data_size(data_size) +{ +} + +void CacheEntryReader::pipe_to(int pipe_fd, Function on_complete, Function on_error) +{ + VERIFY(m_pipe_fd == -1); + m_pipe_fd = pipe_fd; + + m_on_pipe_complete = move(on_complete); + m_on_pipe_error = move(on_error); + + if (m_marked_for_deletion) { + pipe_error(Error::from_string_literal("Cache entry has been deleted")); + return; + } + + m_pipe_write_notifier = Core::Notifier::construct(m_pipe_fd, Core::NotificationType::Write); + m_pipe_write_notifier->set_enabled(false); + + m_pipe_write_notifier->on_activation = [this]() { + m_pipe_write_notifier->set_enabled(false); + pipe_without_blocking(); + }; + + pipe_without_blocking(); +} + +void CacheEntryReader::pipe_without_blocking() +{ + if (m_marked_for_deletion) { + pipe_error(Error::from_string_literal("Cache entry has been deleted")); + return; + } + + auto result = Core::System::transfer_file_through_pipe(m_fd, m_pipe_fd, m_data_offset + m_bytes_piped, m_data_size - m_bytes_piped); + + if (result.is_error()) { + if (result.error().code() != EAGAIN && result.error().code() != EWOULDBLOCK) + pipe_error(result.release_error()); + else + m_pipe_write_notifier->set_enabled(true); + + return; + } + + m_bytes_piped += result.value(); + + if (m_bytes_piped == m_data_size) { + pipe_complete(); + return; + } + + pipe_without_blocking(); +} + +void CacheEntryReader::pipe_complete() +{ + if (auto result = read_and_validate_footer(); result.is_error()) { + dbgln("\033[31;1mError validating cache entry for\033[0m {}: {}", m_url, result.error()); + remove(); + + if (m_on_pipe_error) + m_on_pipe_error(m_bytes_piped); + } else { + m_index.update_last_access_time(m_cache_key); + + if (m_on_pipe_complete) + m_on_pipe_complete(m_bytes_piped); + } + + close_and_destory_cache_entry(); +} + +void CacheEntryReader::pipe_error(Error error) +{ + dbgln("\033[31;1mError transferring cache to pipe for\033[0m {}: {}", m_url, error); + + if (m_on_pipe_error) + m_on_pipe_error(m_bytes_piped); + + close_and_destory_cache_entry(); +} + +ErrorOr CacheEntryReader::read_and_validate_footer() +{ + TRY(m_file->seek(m_data_offset + m_data_size, SeekMode::SetPosition)); + m_cache_footer = TRY(m_file->read_value()); + + if (m_cache_footer.data_size != m_data_size) + return Error::from_string_literal("Invalid data size in footer"); + + // FIXME: Validate the crc. + + return {}; +} + +} diff --git a/Services/RequestServer/Cache/CacheEntry.h b/Services/RequestServer/Cache/CacheEntry.h new file mode 100644 index 00000000000..1c8ab8f8323 --- /dev/null +++ b/Services/RequestServer/Cache/CacheEntry.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RequestServer { + +struct [[gnu::packed]] CacheHeader { + static ErrorOr read_from_stream(Stream&); + ErrorOr write_to_stream(Stream&) const; + + static constexpr auto CACHE_MAGIC = 0xcafef00du; + static constexpr auto CACHE_VERSION = 1; + + u32 magic { CACHE_MAGIC }; + u32 version { CACHE_VERSION }; + + u32 url_size { 0 }; + u32 url_hash { 0 }; + + u32 status_code { 0 }; + u32 reason_phrase_size { 0 }; + u32 reason_phrase_hash { 0 }; + + u32 headers_size { 0 }; + u32 headers_hash { 0 }; +}; + +struct [[gnu::packed]] CacheFooter { + static ErrorOr read_from_stream(Stream&); + ErrorOr write_to_stream(Stream&) const; + + u64 data_size { 0 }; + u32 crc32 { 0 }; +}; + +// A cache entry is an amalgamation of all information needed to reconstruct HTTP responses. It is created once we have +// received the response headers for a request. The body is streamed into the entry as it is received. The cache format +// on disk is: +// +// [CacheHeader][URL][ReasonPhrase][HttpHeaders][Data][CacheFooter] +class CacheEntry { +public: + virtual ~CacheEntry() = default; + + void remove(); + + void mark_for_deletion(Badge) { m_marked_for_deletion = true; } + +protected: + CacheEntry(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, CacheHeader); + + void close_and_destory_cache_entry(); + + DiskCache& m_disk_cache; + CacheIndex& m_index; + + u64 m_cache_key { 0 }; + + String m_url; + LexicalPath m_path; + + CacheHeader m_cache_header; + CacheFooter m_cache_footer; + + bool m_marked_for_deletion { false }; +}; + +class CacheEntryWriter : public CacheEntry { +public: + static ErrorOr> create(DiskCache&, CacheIndex&, u64 cache_key, String url, u32 status_code, Optional reason_phrase, HTTP::HeaderMap const&, UnixDateTime request_time); + virtual ~CacheEntryWriter() override = default; + + ErrorOr write_data(ReadonlyBytes); + ErrorOr flush(); + +private: + CacheEntryWriter(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr, CacheHeader, UnixDateTime request_time); + + NonnullOwnPtr m_file; + + UnixDateTime m_request_time; + UnixDateTime m_response_time; +}; + +class CacheEntryReader : public CacheEntry { +public: + static ErrorOr> create(DiskCache&, CacheIndex&, u64 cache_key, u64 data_size); + virtual ~CacheEntryReader() override = default; + + void pipe_to(int pipe_fd, Function on_complete, Function on_error); + + u32 status_code() const { return m_cache_header.status_code; } + Optional const& reason_phrase() const { return m_reason_phrase; } + HTTP::HeaderMap const& headers() const { return m_headers; } + +private: + CacheEntryReader(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr, int fd, CacheHeader, Optional reason_phrase, HTTP::HeaderMap, u64 data_offset, u64 data_size); + + void pipe_without_blocking(); + void pipe_complete(); + void pipe_error(Error); + + ErrorOr read_and_validate_footer(); + + NonnullOwnPtr m_file; + int m_fd { -1 }; + + RefPtr m_pipe_write_notifier; + int m_pipe_fd { -1 }; + + Function m_on_pipe_complete; + Function m_on_pipe_error; + u64 m_bytes_piped { 0 }; + + Optional m_reason_phrase; + HTTP::HeaderMap m_headers; + + u64 const m_data_offset { 0 }; + u64 const m_data_size { 0 }; +}; + +} diff --git a/Services/RequestServer/Cache/CacheIndex.cpp b/Services/RequestServer/Cache/CacheIndex.cpp new file mode 100644 index 00000000000..9b6da3e020e --- /dev/null +++ b/Services/RequestServer/Cache/CacheIndex.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace RequestServer { + +ErrorOr CacheIndex::create(Database::Database& database) +{ + auto create_table = TRY(database.prepare_statement(R"#( + CREATE TABLE IF NOT EXISTS CacheIndex ( + cache_key INTEGER, + url TEXT, + data_size INTEGER, + request_time INTEGER, + response_time INTEGER, + last_access_time INTEGER, + PRIMARY KEY(cache_key) + );)#"sv)); + database.execute_statement(create_table, {}); + + Statements statements {}; + statements.insert_entry = TRY(database.prepare_statement("INSERT OR REPLACE INTO CacheIndex VALUES (?, ?, ?, ?, ?, ?);"sv)); + statements.remove_entry = TRY(database.prepare_statement("DELETE FROM CacheIndex WHERE cache_key = ?;"sv)); + statements.remove_all_entries = TRY(database.prepare_statement("DELETE FROM CacheIndex;"sv)); + statements.select_entry = TRY(database.prepare_statement("SELECT * FROM CacheIndex WHERE cache_key = ?;"sv)); + statements.update_last_access_time = TRY(database.prepare_statement("UPDATE CacheIndex SET last_access_time = ? WHERE cache_key = ?;"sv)); + + return CacheIndex { database, statements }; +} + +CacheIndex::CacheIndex(Database::Database& database, Statements statements) + : m_database(database) + , m_statements(statements) +{ +} + +void CacheIndex::create_entry(u64 cache_key, String url, u64 data_size, UnixDateTime request_time, UnixDateTime response_time) +{ + auto now = UnixDateTime::now(); + + Entry entry { + .cache_key = cache_key, + .url = move(url), + .data_size = data_size, + .request_time = request_time, + .response_time = response_time, + .last_access_time = now, + }; + + m_database.execute_statement(m_statements.insert_entry, {}, entry.cache_key, entry.url, entry.data_size, entry.request_time, entry.response_time, entry.last_access_time); + m_entries.set(cache_key, move(entry)); +} + +void CacheIndex::remove_entry(u64 cache_key) +{ + m_database.execute_statement(m_statements.remove_entry, {}, cache_key); + m_entries.remove(cache_key); +} + +void CacheIndex::remove_all_entries() +{ + m_database.execute_statement(m_statements.remove_all_entries, {}); + m_entries.clear(); +} + +void CacheIndex::update_last_access_time(u64 cache_key) +{ + auto entry = m_entries.get(cache_key); + if (!entry.has_value()) + return; + + auto now = UnixDateTime::now(); + + m_database.execute_statement(m_statements.update_last_access_time, {}, now, cache_key); + entry->last_access_time = now; +} + +Optional CacheIndex::find_entry(u64 cache_key) +{ + if (auto entry = m_entries.get(cache_key); entry.has_value()) + return entry; + + m_database.execute_statement( + m_statements.select_entry, [&](auto statement_id) { + int column = 0; + + auto cache_key = m_database.result_column(statement_id, column++); + auto url = m_database.result_column(statement_id, column++); + auto data_size = m_database.result_column(statement_id, column++); + auto request_time = m_database.result_column(statement_id, column++); + auto response_time = m_database.result_column(statement_id, column++); + auto last_access_time = m_database.result_column(statement_id, column++); + + Entry entry { cache_key, move(url), data_size, request_time, response_time, last_access_time }; + m_entries.set(cache_key, move(entry)); + }, + cache_key); + + return m_entries.get(cache_key); +} + +} diff --git a/Services/RequestServer/Cache/CacheIndex.h b/Services/RequestServer/Cache/CacheIndex.h new file mode 100644 index 00000000000..47fe55a0853 --- /dev/null +++ b/Services/RequestServer/Cache/CacheIndex.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace RequestServer { + +// The cache index is a SQL database containing metadata about each cache entry. An entry in the index is created once +// the entire cache entry has been successfully written to disk. +class CacheIndex { + struct Entry { + u64 cache_key { 0 }; + + String url; + u64 data_size { 0 }; + + UnixDateTime request_time; + UnixDateTime response_time; + UnixDateTime last_access_time; + }; + +public: + static ErrorOr create(Database::Database&); + + void create_entry(u64 cache_key, String url, u64 data_size, UnixDateTime request_time, UnixDateTime response_time); + void remove_entry(u64 cache_key); + void remove_all_entries(); + + Optional find_entry(u64 cache_key); + + void update_last_access_time(u64 cache_key); + +private: + struct Statements { + Database::StatementID insert_entry { 0 }; + Database::StatementID remove_entry { 0 }; + Database::StatementID remove_all_entries { 0 }; + Database::StatementID select_entry { 0 }; + Database::StatementID update_last_access_time { 0 }; + }; + + CacheIndex(Database::Database&, Statements); + + Database::Database& m_database; + Statements m_statements; + + HashMap m_entries; +}; + +} diff --git a/Services/RequestServer/Cache/DiskCache.cpp b/Services/RequestServer/Cache/DiskCache.cpp new file mode 100644 index 00000000000..f0cf3a8c093 --- /dev/null +++ b/Services/RequestServer/Cache/DiskCache.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace RequestServer { + +static constexpr auto INDEX_DATABASE = "INDEX"sv; + +ErrorOr DiskCache::create() +{ + auto cache_directory = LexicalPath::join(Core::StandardPaths::cache_directory(), "Ladybird"sv, "Cache"sv); + + auto database = TRY(Database::Database::create(cache_directory.string(), INDEX_DATABASE)); + auto index = TRY(CacheIndex::create(database)); + + return DiskCache { move(database), move(cache_directory), move(index) }; +} + +DiskCache::DiskCache(NonnullRefPtr database, LexicalPath cache_directory, CacheIndex index) + : m_database(move(database)) + , m_cache_directory(move(cache_directory)) + , m_index(move(index)) +{ +} + +Optional DiskCache::create_entry(URL::URL const& url, StringView method, u32 status_code, Optional reason_phrase, HTTP::HeaderMap const& headers, UnixDateTime request_time) +{ + if (!is_cacheable(method, status_code, headers)) + return {}; + + if (auto freshness = calculate_freshness_lifetime(headers); freshness.is_negative() || freshness.is_zero()) + return {}; + + auto serialized_url = serialize_url_for_cache_storage(url); + auto cache_key = create_cache_key(serialized_url, method); + + auto cache_entry = CacheEntryWriter::create(*this, m_index, cache_key, move(serialized_url), status_code, move(reason_phrase), headers, request_time); + if (cache_entry.is_error()) { + dbgln("\033[31;1mUnable to create cache entry for\033[0m {}: {}", url, cache_entry.error()); + return {}; + } + + dbgln("\033[32;1mCreated disk cache entry for\033[0m {}", url); + + auto address = reinterpret_cast(cache_entry.value().ptr()); + m_open_cache_entries.set(address, cache_entry.release_value()); + + return static_cast(**m_open_cache_entries.get(address)); +} + +Optional DiskCache::open_entry(URL::URL const& url, StringView method) +{ + auto serialized_url = serialize_url_for_cache_storage(url); + auto cache_key = create_cache_key(serialized_url, method); + + auto index_entry = m_index.find_entry(cache_key); + if (!index_entry.has_value()) { + dbgln("\033[35;1mNo disk cache entry for\033[0m {}", url); + return {}; + } + + auto cache_entry = CacheEntryReader::create(*this, m_index, cache_key, index_entry->data_size); + if (cache_entry.is_error()) { + dbgln("\033[31;1mUnable to open cache entry for\033[0m {}: {}", url, cache_entry.error()); + m_index.remove_entry(cache_key); + return {}; + } + + auto freshness_lifetime = calculate_freshness_lifetime(cache_entry.value()->headers()); + auto current_age = calculate_age(cache_entry.value()->headers(), index_entry->request_time, index_entry->response_time); + + if (!is_response_fresh(freshness_lifetime, current_age)) { + dbgln("\033[33;1mCache entry expired for\033[0m {} (lifetime={}s age={}s)", url, freshness_lifetime.to_seconds(), current_age.to_seconds()); + cache_entry.value()->remove(); + return {}; + } + + dbgln("\033[32;1mOpened disk cache entry for\033[0m {} (lifetime={}s age={}s) ({} bytes)", url, freshness_lifetime.to_seconds(), current_age.to_seconds(), index_entry->data_size); + + auto address = reinterpret_cast(cache_entry.value().ptr()); + m_open_cache_entries.set(address, cache_entry.release_value()); + + return static_cast(**m_open_cache_entries.get(address)); +} + +void DiskCache::clear_cache() +{ + for (auto& [_, cache_entry] : m_open_cache_entries) + cache_entry->mark_for_deletion({}); + + m_index.remove_all_entries(); + + Core::DirIterator it { m_cache_directory.string(), Core::DirIterator::SkipDots }; + size_t cache_entries { 0 }; + + while (it.has_next()) { + auto entry = it.next_full_path(); + if (LexicalPath { entry }.title() == INDEX_DATABASE) + continue; + + (void)FileSystem::remove(entry, FileSystem::RecursionMode::Disallowed); + ++cache_entries; + } + + dbgln("Cleared {} disk cache entries", cache_entries); +} + +void DiskCache::cache_entry_closed(Badge, CacheEntry const& cache_entry) +{ + auto address = reinterpret_cast(&cache_entry); + m_open_cache_entries.remove(address); +} + +} diff --git a/Services/RequestServer/Cache/DiskCache.h b/Services/RequestServer/Cache/DiskCache.h new file mode 100644 index 00000000000..9f21dd1ae8b --- /dev/null +++ b/Services/RequestServer/Cache/DiskCache.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RequestServer { + +class DiskCache { +public: + static ErrorOr create(); + + Optional create_entry(URL::URL const&, StringView method, u32 status_code, Optional reason_phrase, HTTP::HeaderMap const&, UnixDateTime request_time); + Optional open_entry(URL::URL const&, StringView method); + void clear_cache(); + + LexicalPath const& cache_directory() { return m_cache_directory; } + + void cache_entry_closed(Badge, CacheEntry const&); + +private: + DiskCache(NonnullRefPtr, LexicalPath cache_directory, CacheIndex); + + NonnullRefPtr m_database; + + HashMap> m_open_cache_entries; + + LexicalPath m_cache_directory; + CacheIndex m_index; +}; + +} diff --git a/Services/RequestServer/Cache/Utilities.cpp b/Services/RequestServer/Cache/Utilities.cpp new file mode 100644 index 00000000000..6d240d097a2 --- /dev/null +++ b/Services/RequestServer/Cache/Utilities.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace RequestServer { + +static Optional extract_cache_control_directive(StringView cache_control, StringView directive) +{ + Optional result; + + cache_control.for_each_split_view(","sv, SplitBehavior::Nothing, [&](StringView candidate) { + if (!candidate.contains(directive, CaseSensitivity::CaseInsensitive)) + return IterationDecision::Continue; + + auto index = candidate.find('='); + if (!index.has_value()) + return IterationDecision::Continue; + + result = candidate.substring_view(*index + 1); + return IterationDecision::Break; + }); + + return result; +} + +// https://httpwg.org/specs/rfc9110.html#field.date +static Optional parse_http_date(Optional date) +{ + // , :: GMT + if (date.has_value()) + return UnixDateTime::parse("%a, %d %b %Y %T GMT"sv, *date, true); + return {}; +} + +String serialize_url_for_cache_storage(URL::URL const& url) +{ + if (!url.fragment().has_value()) + return url.serialize(); + + auto sanitized = url; + sanitized.set_fragment({}); + return sanitized.serialize(); +} + +u64 create_cache_key(StringView url, StringView method) +{ + auto hasher = Crypto::Hash::SHA1::create(); + hasher->update(url); + hasher->update(method); + + auto digest = hasher->digest(); + auto bytes = digest.bytes(); + + u64 result = 0; + result |= static_cast(bytes[0]) << 56; + result |= static_cast(bytes[1]) << 48; + result |= static_cast(bytes[2]) << 40; + result |= static_cast(bytes[3]) << 32; + result |= static_cast(bytes[4]) << 24; + result |= static_cast(bytes[5]) << 16; + result |= static_cast(bytes[6]) << 8; + result |= static_cast(bytes[7]); + + return result; +} + +// https://httpwg.org/specs/rfc9111.html#response.cacheability +bool is_cacheable(StringView method, u32 status_code, HTTP::HeaderMap const& headers) +{ + // A cache MUST NOT store a response to a request unless: + + // * the request method is understood by the cache; + if (!method.is_one_of("GET"sv, "HEAD"sv)) + return false; + + // * the response status code is final (see Section 15 of [HTTP]); + if (status_code < 200) + return false; + + auto cache_control = headers.get("Cache-Control"sv); + if (!cache_control.has_value()) + return false; + + // * if the response status code is 206 or 304, or the must-understand cache directive (see Section 5.2.2.3) is + // present: the cache understands the response status code; + + // * the no-store cache directive is not present in the response (see Section 5.2.2.5); + if (cache_control->contains("no-store"sv, CaseSensitivity::CaseInsensitive)) + return false; + + // * if the cache is shared: the private response directive is either not present or allows a shared cache to store + // a modified response; see Section 5.2.2.7); + + // * if the cache is shared: the Authorization header field is not present in the request (see Section 11.6.2 of + // [HTTP]) or a response directive is present that explicitly allows shared caching (see Section 3.5); and + + // * the response contains at least one of the following: + // - a public response directive (see Section 5.2.2.9); + // - a private response directive, if the cache is not shared (see Section 5.2.2.7); + // - an Expires header field (see Section 5.3); + // - a max-age response directive (see Section 5.2.2.1); + // - if the cache is shared: an s-maxage response directive (see Section 5.2.2.10); + // - a cache extension that allows it to be cached (see Section 5.2.3); or + // - a status code that is defined as heuristically cacheable (see Section 4.2.2). + + // FIXME: Implement cache revalidation. + if (cache_control->contains("no-cache"sv, CaseSensitivity::CaseInsensitive)) + return false; + if (cache_control->contains("revalidate"sv, CaseSensitivity::CaseInsensitive)) + return false; + + return true; +} + +// https://httpwg.org/specs/rfc9111.html#storing.fields +bool is_header_exempted_from_storage(StringView name) +{ + // Caches MUST include all received response header fields — including unrecognized ones — when storing a response; + // this assures that new HTTP header fields can be successfully deployed. However, the following exceptions are made: + return name.is_one_of_ignoring_ascii_case( + // * The Connection header field and fields whose names are listed in it are required by Section 7.6.1 of [HTTP] + // to be removed before forwarding the message. This MAY be implemented by doing so before storage. + "Connection"sv, + "Keep-Alive"sv, + "Proxy-Connection"sv, + "TE"sv, + "Transfer-Encoding"sv, + "Upgrade"sv + + // * Likewise, some fields' semantics require them to be removed before forwarding the message, and this MAY be + // implemented by doing so before storage; see Section 7.6.1 of [HTTP] for some examples. + + // * The no-cache (Section 5.2.2.4) and private (Section 5.2.2.7) cache directives can have arguments that + // prevent storage of header fields by all caches and shared caches, respectively. + + // * Header fields that are specific to the proxy that a cache uses when forwarding a request MUST NOT be stored, + // unless the cache incorporates the identity of the proxy into the cache key. Effectively, this is limited to + // Proxy-Authenticate (Section 11.7.1 of [HTTP]), Proxy-Authentication-Info (Section 11.7.3 of [HTTP]), and + // Proxy-Authorization (Section 11.7.2 of [HTTP]). + ); +} + +// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime +AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const& headers) +{ + // A cache can calculate the freshness lifetime (denoted as freshness_lifetime) of a response by evaluating the + // following rules and using the first match: + + // * If the cache is shared and the s-maxage response directive (Section 5.2.2.10) is present, use its value, or + + // * If the max-age response directive (Section 5.2.2.1) is present, use its value, or + if (auto cache_control = headers.get("Cache-Control"sv); cache_control.has_value()) { + if (auto max_age = extract_cache_control_directive(*cache_control, "max-age"sv); max_age.has_value()) { + if (auto seconds = max_age->to_number(); seconds.has_value()) + return AK::Duration::from_seconds(*seconds); + } + } + + // * If the Expires response header field (Section 5.3) is present, use its value minus the value of the Date response + // header field (using the time the message was received if it is not present, as per Section 6.6.1 of [HTTP]), or + if (auto expires = parse_http_date(headers.get("Expires"sv)); expires.has_value()) { + auto date = parse_http_date(headers.get("Date"sv)).value_or_lazy_evaluated([]() { + return UnixDateTime::now(); + }); + + return *expires - date; + } + + // * Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be + // applicable; see Section 4.2.2. + + return {}; +} + +// https://httpwg.org/specs/rfc9111.html#age.calculations +AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_time, UnixDateTime response_time) +{ + // The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic + // operation; or 0, if not available. + AK::Duration age_value; + + if (auto age = headers.get("Age"sv); age.has_value()) { + if (auto seconds = age->to_number(); seconds.has_value()) + age_value = AK::Duration::from_seconds(*seconds); + } + + // The term "now" means the current value of this implementation's clock (Section 5.6.7 of [HTTP]). + auto now = UnixDateTime::now(); + + // The term "date_value" denotes the value of the Date header field, in a form appropriate for arithmetic operations. + // See Section 6.6.1 of [HTTP] for the definition of the Date header field and for requirements regarding responses + // without it. + auto date_value = parse_http_date(headers.get("Date"sv)).value_or(now); + + auto apparent_age = max(0LL, (response_time - date_value).to_seconds()); + + auto response_delay = response_time - request_time; + auto corrected_age_value = age_value + response_delay; + + auto corrected_initial_age = max(apparent_age, corrected_age_value.to_seconds()); + + auto resident_time = (now - response_time).to_seconds(); + auto current_age = corrected_initial_age + resident_time; + + return AK::Duration::from_seconds(current_age); +} + +// https://httpwg.org/specs/rfc9111.html#expiration.model +bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age) +{ + return freshness_lifetime > current_age; +} + +} diff --git a/Services/RequestServer/Cache/Utilities.h b/Services/RequestServer/Cache/Utilities.h new file mode 100644 index 00000000000..78f4c6d21de --- /dev/null +++ b/Services/RequestServer/Cache/Utilities.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace RequestServer { + +String serialize_url_for_cache_storage(URL::URL const&); +u64 create_cache_key(StringView url, StringView method); + +bool is_cacheable(StringView method, u32 status_code, HTTP::HeaderMap const&); +bool is_header_exempted_from_storage(StringView name); + +AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const&); +AK::Duration calculate_age(HTTP::HeaderMap const&, UnixDateTime request_time, UnixDateTime response_time); +bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age); + +} diff --git a/Services/RequestServer/ConnectionFromClient.cpp b/Services/RequestServer/ConnectionFromClient.cpp index e598e460d8b..95583e84509 100644 --- a/Services/RequestServer/ConnectionFromClient.cpp +++ b/Services/RequestServer/ConnectionFromClient.cpp @@ -20,12 +20,15 @@ #include #include #include +#include #include #include + #ifdef AK_OS_WINDOWS // needed because curl.h includes winsock2.h # include #endif + #include namespace RequestServer { @@ -42,6 +45,8 @@ static struct { bool validate_dnssec_locally = false; } g_dns_info; +Optional g_disk_cache; + static WeakPtr s_resolver {}; static NonnullRefPtr default_resolver() { @@ -116,13 +121,17 @@ struct ConnectionFromClient::ActiveRequest : public Weakable { bool got_all_headers { false }; bool is_connect_only { false }; size_t downloaded_so_far { 0 }; - String url; + URL::URL url; + ByteString method; Optional reason_phrase; ByteBuffer body; AllocatingMemoryStream send_buffer; NonnullRefPtr write_notifier; bool done_fetching { false }; + Optional cache_entry; + UnixDateTime request_start_time; + ActiveRequest(ConnectionFromClient& client, CURLM* multi, CURL* easy, i32 request_id, int writer_fd) : multi(multi) , easy(easy) @@ -130,6 +139,7 @@ struct ConnectionFromClient::ActiveRequest : public Weakable { , client(client) , writer_fd(writer_fd) , write_notifier(Core::Notifier::construct(writer_fd, Core::NotificationType::Write)) + , request_start_time(UnixDateTime::now()) { write_notifier->set_enabled(false); write_notifier->on_activation = [this] { @@ -163,6 +173,13 @@ struct ConnectionFromClient::ActiveRequest : public Weakable { return {}; } + if (cache_entry.has_value()) { + auto bytes_sent = bytes_to_send.span().slice(0, result.value()); + + if (cache_entry->write_data(bytes_sent).is_error()) + cache_entry.clear(); + } + MUST(send_buffer.discard(result.value())); write_notifier->set_enabled(!send_buffer.is_eof()); if (send_buffer.is_eof() && done_fetching) @@ -193,6 +210,9 @@ struct ConnectionFromClient::ActiveRequest : public Weakable { for (auto* string_list : curl_string_lists) curl_slist_free_all(string_list); + + if (cache_entry.has_value()) + (void)cache_entry->flush(); } void flush_headers_if_needed() @@ -204,6 +224,9 @@ struct ConnectionFromClient::ActiveRequest : public Weakable { auto result = curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_status_code); VERIFY(result == CURLE_OK); client->async_headers_became_available(request_id, headers, http_status_code, reason_phrase); + + if (g_disk_cache.has_value()) + cache_entry = g_disk_cache->create_entry(url, method, http_status_code, reason_phrase, headers, request_start_time); } }; @@ -464,6 +487,33 @@ void ConnectionFromClient::start_request(i32, ByteString, URL::URL, HTTP::Header void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL::URL url, HTTP::HeaderMap request_headers, ByteBuffer request_body, Core::ProxyData proxy_data) { dbgln_if(REQUESTSERVER_DEBUG, "RequestServer: start_request({}, {})", request_id, url); + + if (g_disk_cache.has_value()) { + if (auto cache_entry = g_disk_cache->open_entry(url, method); cache_entry.has_value()) { + auto fds = MUST(Core::System::pipe2(O_NONBLOCK)); + auto writer_fd = fds[1]; + auto reader_fd = fds[0]; + + async_request_started(request_id, IPC::File::adopt_fd(reader_fd)); + async_headers_became_available(request_id, cache_entry->headers(), cache_entry->status_code(), cache_entry->reason_phrase()); + + cache_entry->pipe_to( + writer_fd, + [this, request_id, writer_fd](auto bytes_sent) { + // FIXME: Implement timing info for cache hits. + async_request_finished(request_id, bytes_sent, {}, {}); + MUST(Core::System::close(writer_fd)); + }, + [this, request_id, writer_fd](auto bytes_sent) { + // FIXME: We should switch to a network request automatically if reading from cache has failed. + async_request_finished(request_id, bytes_sent, {}, Requests::NetworkError::CacheReadFailed); + (void)Core::System::close(writer_fd); + }); + + return; + } + } + auto host = url.serialized_host().to_byte_string(); m_resolver->dns.lookup(host, DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA }, { .validate_dnssec_locally = g_dns_info.validate_dnssec_locally }) @@ -500,7 +550,8 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL: async_request_started(request_id, IPC::File::adopt_fd(reader_fd)); auto request = make(*this, m_curl_multi, easy, request_id, writer_fd); - request->url = url.to_string(); + request->url = url; + request->method = method; auto set_option = [easy](auto option, auto value) { auto result = curl_easy_setopt(easy, option, value); @@ -760,8 +811,6 @@ Messages::RequestServer::SetCertificateResponse ConnectionFromClient::set_certif void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) { - auto const url_string_value = url.to_string(); - if (cache_level == CacheLevel::CreateConnection) { auto* easy = curl_easy_init(); if (!easy) { @@ -781,11 +830,11 @@ void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::Cach auto connect_only_request_id = get_random(); auto request = make(*this, m_curl_multi, easy, connect_only_request_id, 0); - request->url = url_string_value; + request->url = url; request->is_connect_only = true; set_option(CURLOPT_PRIVATE, request.ptr()); - set_option(CURLOPT_URL, url_string_value.to_byte_string().characters()); + set_option(CURLOPT_URL, url.to_byte_string().characters()); set_option(CURLOPT_PORT, url.port_or_default()); set_option(CURLOPT_CONNECTTIMEOUT, s_connect_timeout_seconds); set_option(CURLOPT_CONNECT_ONLY, 1L); @@ -812,6 +861,12 @@ void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::Cach } } +void ConnectionFromClient::clear_cache() +{ + if (g_disk_cache.has_value()) + g_disk_cache->clear_cache(); +} + void ConnectionFromClient::websocket_connect(i64 websocket_id, URL::URL url, ByteString origin, Vector protocols, Vector extensions, HTTP::HeaderMap additional_request_headers) { auto host = url.serialized_host().to_byte_string(); diff --git a/Services/RequestServer/ConnectionFromClient.h b/Services/RequestServer/ConnectionFromClient.h index 0a6baaa1618..7eafa4f1b0f 100644 --- a/Services/RequestServer/ConnectionFromClient.h +++ b/Services/RequestServer/ConnectionFromClient.h @@ -49,6 +49,8 @@ private: virtual Messages::RequestServer::SetCertificateResponse set_certificate(i32, ByteString, ByteString) override; virtual void ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) override; + virtual void clear_cache() override; + virtual void websocket_connect(i64 websocket_id, URL::URL, ByteString, Vector, Vector, HTTP::HeaderMap) override; virtual void websocket_send(i64 websocket_id, bool, ByteBuffer) override; virtual void websocket_close(i64 websocket_id, u16, ByteString) override; diff --git a/Services/RequestServer/Forward.h b/Services/RequestServer/Forward.h new file mode 100644 index 00000000000..e78918ece1f --- /dev/null +++ b/Services/RequestServer/Forward.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +namespace RequestServer { + +class CacheEntry; +class CacheEntryReader; +class CacheEntryWriter; +class CacheIndex; +class DiskCache; + +} diff --git a/Services/RequestServer/RequestServer.ipc b/Services/RequestServer/RequestServer.ipc index ea4979d66b4..848efc91a97 100644 --- a/Services/RequestServer/RequestServer.ipc +++ b/Services/RequestServer/RequestServer.ipc @@ -22,6 +22,8 @@ endpoint RequestServer ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) =| + clear_cache() =| + // Websocket Connection API websocket_connect(i64 websocket_id, URL::URL url, ByteString origin, Vector protocols, Vector extensions, HTTP::HeaderMap additional_request_headers) =| websocket_send(i64 websocket_id, bool is_text, ByteBuffer data) =| diff --git a/Services/RequestServer/main.cpp b/Services/RequestServer/main.cpp index ae2f16dffc9..67561184dad 100644 --- a/Services/RequestServer/main.cpp +++ b/Services/RequestServer/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #if defined(AK_OS_MACOS) @@ -23,6 +24,7 @@ namespace RequestServer { extern ByteString g_default_certificate_path; +extern Optional g_disk_cache; } @@ -32,11 +34,13 @@ ErrorOr ladybird_main(Main::Arguments arguments) Vector certificates; StringView mach_server_name; + bool enable_http_disk_cache = false; bool wait_for_debugger = false; Core::ArgsParser args_parser; args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate"); args_parser.add_option(mach_server_name, "Mach server name", "mach-server-name", 0, "mach_server_name"); + args_parser.add_option(enable_http_disk_cache, "Enable HTTP disk cache", "enable-http-disk-cache"); args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger"); args_parser.parse(arguments); @@ -54,6 +58,13 @@ ErrorOr ladybird_main(Main::Arguments arguments) Core::Platform::register_with_mach_server(mach_server_name); #endif + if (enable_http_disk_cache) { + if (auto cache = RequestServer::DiskCache::create(); cache.is_error()) + warnln("Unable to create disk cache: {}", cache.error()); + else + RequestServer::g_disk_cache = cache.release_value(); + } + auto client = TRY(IPC::take_over_accepted_client_from_system_server()); return event_loop.exec(); diff --git a/Tests/LibWeb/Ref/expected/position-absolute-fixed-nested-static-position.html b/Tests/LibWeb/Ref/expected/position-absolute-fixed-nested-static-position.html new file mode 100644 index 00000000000..047c1ceb276 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/position-absolute-fixed-nested-static-position.html @@ -0,0 +1,9 @@ + + +
diff --git a/Tests/LibWeb/Ref/input/position-absolute-and-fixed-nested-static-position.html b/Tests/LibWeb/Ref/input/position-absolute-and-fixed-nested-static-position.html new file mode 100644 index 00000000000..f5e70086338 --- /dev/null +++ b/Tests/LibWeb/Ref/input/position-absolute-and-fixed-nested-static-position.html @@ -0,0 +1,21 @@ + + + +
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/input/position-absolute-nested-static-position.html b/Tests/LibWeb/Ref/input/position-absolute-nested-static-position.html new file mode 100644 index 00000000000..983eb4356c7 --- /dev/null +++ b/Tests/LibWeb/Ref/input/position-absolute-nested-static-position.html @@ -0,0 +1,20 @@ + + + +
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/input/position-fixed-and-absolute-nested-static-position.html b/Tests/LibWeb/Ref/input/position-fixed-and-absolute-nested-static-position.html new file mode 100644 index 00000000000..f93dcc9d1c4 --- /dev/null +++ b/Tests/LibWeb/Ref/input/position-fixed-and-absolute-nested-static-position.html @@ -0,0 +1,21 @@ + + + +
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Ref/input/position-fixed-nested-static-position.html b/Tests/LibWeb/Ref/input/position-fixed-nested-static-position.html new file mode 100644 index 00000000000..26c421937af --- /dev/null +++ b/Tests/LibWeb/Ref/input/position-fixed-nested-static-position.html @@ -0,0 +1,20 @@ + + + +
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-properties-values-api/register-property-syntax-parsing.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-properties-values-api/register-property-syntax-parsing.txt new file mode 100644 index 00000000000..747edcfe9be --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-properties-values-api/register-property-syntax-parsing.txt @@ -0,0 +1,250 @@ +Harness status: OK + +Found 239 tests + +189 Pass +50 Fail +Pass syntax:'*', initialValue:'a' is valid +Pass syntax:' * ', initialValue:'b' is valid +Pass syntax:'', initialValue:'2px' is valid +Pass syntax:' ', initialValue:'5' is valid +Pass syntax:' ', initialValue:'10%' is valid +Pass syntax:'+', initialValue:'red' is valid +Pass syntax:' + | ', initialValue:'2px 8px' is valid +Pass syntax:' + | #', initialValue:'red, blue' is valid +Pass syntax:'||', initialValue:'2px' is valid +Pass syntax:' | | | | ', initialValue:'red' is valid +Pass syntax:'