diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 3d853c53aa8..4363f0782ed 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -28,6 +28,7 @@ set(SOURCES Bindings/SyntheticHostDefined.cpp Clipboard/Clipboard.cpp Clipboard/ClipboardEvent.cpp + Compression/CompressionStream.cpp Crypto/Crypto.cpp Crypto/CryptoAlgorithms.cpp Crypto/CryptoBindings.cpp @@ -850,7 +851,7 @@ set(GENERATED_SOURCES serenity_lib(LibWeb web) -target_link_libraries(LibWeb PRIVATE LibCore LibCrypto LibJS LibHTTP LibGfx LibIPC LibRegex LibSyntax LibTextCodec LibUnicode LibMedia LibWasm LibXML LibIDL LibURL LibTLS LibRequests LibGC skia) +target_link_libraries(LibWeb PRIVATE LibCore LibCompress LibCrypto LibJS LibHTTP LibGfx LibIPC LibRegex LibSyntax LibTextCodec LibUnicode LibMedia LibWasm LibXML LibIDL LibURL LibTLS LibRequests LibGC skia) generate_js_bindings(LibWeb) diff --git a/Libraries/LibWeb/Compression/CompressionStream.cpp b/Libraries/LibWeb/Compression/CompressionStream.cpp new file mode 100644 index 00000000000..80419f01fed --- /dev/null +++ b/Libraries/LibWeb/Compression/CompressionStream.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Compression { + +GC_DEFINE_ALLOCATOR(CompressionStream); + +// https://compression.spec.whatwg.org/#dom-compressionstream-compressionstream +WebIDL::ExceptionOr> CompressionStream::construct_impl(JS::Realm& realm, Bindings::CompressionFormat format) +{ + // 1. If format is unsupported in CompressionStream, then throw a TypeError. + // 2. Set this's format to format. + auto input_stream = make(); + + auto compressor = [&, input_stream = MaybeOwned { *input_stream }]() mutable -> ErrorOr { + switch (format) { + case Bindings::CompressionFormat::Deflate: + return TRY(Compress::ZlibCompressor::construct(move(input_stream))); + case Bindings::CompressionFormat::DeflateRaw: + return TRY(Compress::DeflateCompressor::construct(make(move(input_stream)))); + case Bindings::CompressionFormat::Gzip: + return TRY(Compress::GzipCompressor::create(move(input_stream))); + } + + VERIFY_NOT_REACHED(); + }(); + + if (compressor.is_error()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unable to create compressor: {}", compressor.error())) }; + + // 5. Set this's transform to a new TransformStream. + // NOTE: We do this first so that we may store it as nonnull in the GenericTransformStream. + auto stream = realm.create(realm, realm.create(realm), compressor.release_value(), move(input_stream)); + + // 3. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the compress and enqueue a chunk + // algorithm with this and chunk. + auto transform_algorithm = GC::create_function(realm.heap(), [stream](JS::Value chunk) -> GC::Ref { + auto& realm = stream->realm(); + auto& vm = realm.vm(); + + if (auto result = stream->compress_and_enqueue_chunk(chunk); result.is_error()) { + auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, result.exception()); + return WebIDL::create_rejected_promise(realm, *throw_completion.release_value()); + } + + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + }); + + // 4. Let flushAlgorithm be an algorithm which takes no argument and runs the compress flush and enqueue algorithm with this. + auto flush_algorithm = GC::create_function(realm.heap(), [stream]() -> GC::Ref { + auto& realm = stream->realm(); + auto& vm = realm.vm(); + + if (auto result = stream->compress_flush_and_enqueue(); result.is_error()) { + auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, result.exception()); + return WebIDL::create_rejected_promise(realm, *throw_completion.release_value()); + } + + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + }); + + // 6. Set up this's transform with transformAlgorithm set to transformAlgorithm and flushAlgorithm set to flushAlgorithm. + Streams::transform_stream_set_up(stream->m_transform, transform_algorithm, flush_algorithm); + + return stream; +} + +CompressionStream::CompressionStream(JS::Realm& realm, GC::Ref transform, Compressor compressor, NonnullOwnPtr input_stream) + : Bindings::PlatformObject(realm) + , Streams::GenericTransformStreamMixin(transform) + , m_compressor(move(compressor)) + , m_output_stream(move(input_stream)) +{ +} + +CompressionStream::~CompressionStream() = default; + +void CompressionStream::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(CompressionStream); +} + +void CompressionStream::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + Streams::GenericTransformStreamMixin::visit_edges(visitor); +} + +// https://compression.spec.whatwg.org/#compress-and-enqueue-a-chunk +WebIDL::ExceptionOr CompressionStream::compress_and_enqueue_chunk(JS::Value chunk) +{ + auto& realm = this->realm(); + + // 1. If chunk is not a BufferSource type, then throw a TypeError. + if (!WebIDL::is_buffer_source_type(chunk)) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Chunk is not a BufferSource type"sv }; + + // 2. Let buffer be the result of compressing chunk with cs's format and context. + auto buffer = [&]() -> ErrorOr { + if (auto buffer = WebIDL::underlying_buffer_source(chunk.as_object())) + return compress(buffer->buffer(), Finish::No); + return ByteBuffer {}; + }(); + + if (buffer.is_error()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unable to compress chunk: {}", buffer.error())) }; + + // 3. If buffer is empty, return. + if (buffer.value().is_empty()) + return {}; + + // 4. Split buffer into one or more non-empty pieces and convert them into Uint8Arrays. + auto array_buffer = JS::ArrayBuffer::create(realm, buffer.release_value()); + auto array = JS::Uint8Array::create(realm, array_buffer->byte_length(), *array_buffer); + + // 5. For each Uint8Array array, enqueue array in cs's transform. + TRY(Streams::transform_stream_default_controller_enqueue(*m_transform->controller(), array)); + return {}; +} + +// https://compression.spec.whatwg.org/#compress-flush-and-enqueue +WebIDL::ExceptionOr CompressionStream::compress_flush_and_enqueue() +{ + auto& realm = this->realm(); + + // 1. Let buffer be the result of compressing an empty input with cs's format and context, with the finish flag. + auto buffer = compress({}, Finish::Yes); + + if (buffer.is_error()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unable to compress flush: {}", buffer.error())) }; + + // 2. If buffer is empty, return. + if (buffer.value().is_empty()) + return {}; + + // 3. Split buffer into one or more non-empty pieces and convert them into Uint8Arrays. + auto array_buffer = JS::ArrayBuffer::create(realm, buffer.release_value()); + auto array = JS::Uint8Array::create(realm, array_buffer->byte_length(), *array_buffer); + + // 4. For each Uint8Array array, enqueue array in cs's transform. + TRY(Streams::transform_stream_default_controller_enqueue(*m_transform->controller(), array)); + return {}; +} + +ErrorOr CompressionStream::compress(ReadonlyBytes bytes, Finish finish) +{ + TRY(m_compressor.visit([&](auto const& compressor) { + return compressor->write_until_depleted(bytes); + })); + + if (finish == Finish::Yes) { + TRY(m_compressor.visit( + [&](NonnullOwnPtr const& compressor) { + return compressor->finish(); + }, + [&](NonnullOwnPtr const& compressor) { + return compressor->final_flush(); + }, + [&](NonnullOwnPtr const& compressor) { + return compressor->finish(); + })); + } + + auto buffer = TRY(ByteBuffer::create_uninitialized(m_output_stream->used_buffer_size())); + TRY(m_output_stream->read_until_filled(buffer.bytes())); + + return buffer; +} + +} diff --git a/Libraries/LibWeb/Compression/CompressionStream.h b/Libraries/LibWeb/Compression/CompressionStream.h new file mode 100644 index 00000000000..e45cfefc60f --- /dev/null +++ b/Libraries/LibWeb/Compression/CompressionStream.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Compression { + +using Compressor = Variant< + NonnullOwnPtr, + NonnullOwnPtr, + NonnullOwnPtr>; + +// https://compression.spec.whatwg.org/#compressionstream +class CompressionStream final + : public Bindings::PlatformObject + , public Streams::GenericTransformStreamMixin { + WEB_PLATFORM_OBJECT(CompressionStream, Bindings::PlatformObject); + GC_DECLARE_ALLOCATOR(CompressionStream); + +public: + static WebIDL::ExceptionOr> construct_impl(JS::Realm&, Bindings::CompressionFormat); + virtual ~CompressionStream() override; + +private: + CompressionStream(JS::Realm&, GC::Ref, Compressor, NonnullOwnPtr); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + WebIDL::ExceptionOr compress_and_enqueue_chunk(JS::Value); + WebIDL::ExceptionOr compress_flush_and_enqueue(); + + enum class Finish { + No, + Yes, + }; + ErrorOr compress(ReadonlyBytes, Finish); + + Compressor m_compressor; + NonnullOwnPtr m_output_stream; +}; + +} diff --git a/Libraries/LibWeb/Compression/CompressionStream.idl b/Libraries/LibWeb/Compression/CompressionStream.idl new file mode 100644 index 00000000000..42ccf24fe9f --- /dev/null +++ b/Libraries/LibWeb/Compression/CompressionStream.idl @@ -0,0 +1,16 @@ +#import + +// https://compression.spec.whatwg.org/#enumdef-compressionformat +enum CompressionFormat { + "deflate", + "deflate-raw", + "gzip", +}; + +// https://compression.spec.whatwg.org/#compressionstream +[Exposed=*] +interface CompressionStream { + constructor(CompressionFormat format); +}; + +CompressionStream includes GenericTransformStream; diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 28da88a3d5f..c5ff5cd60aa 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -86,6 +86,10 @@ namespace Web::Clipboard { class Clipboard; } +namespace Web::Compression { +class CompressionStream; +} + namespace Web::Cookie { struct Cookie; struct ParsedCookie; diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 7262cbd3035..9cd63ad15ce 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -9,6 +9,7 @@ libweb_js_bindings(Animations/DocumentTimeline) libweb_js_bindings(Animations/KeyframeEffect) libweb_js_bindings(Clipboard/Clipboard) libweb_js_bindings(Clipboard/ClipboardEvent) +libweb_js_bindings(Compression/CompressionStream) libweb_js_bindings(Crypto/Crypto) libweb_js_bindings(Crypto/CryptoKey) libweb_js_bindings(Crypto/SubtleCrypto) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h index b4fab803fea..f1f9de0a9f5 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h @@ -15,6 +15,7 @@ namespace IDL { static constexpr Array libweb_interface_namespaces = { "CSS"sv, "Clipboard"sv, + "Compression"sv, "Crypto"sv, "DOM"sv, "DOMParsing"sv, diff --git a/Tests/LibWeb/Text/expected/Compression/CompressionStream.txt b/Tests/LibWeb/Text/expected/Compression/CompressionStream.txt new file mode 100644 index 00000000000..f9dc0b7f781 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Compression/CompressionStream.txt @@ -0,0 +1,3 @@ +format=deflate: eJwLT83JUchIzcnJV0grykzNSylWBABGEQb1 +format=deflate-raw: C0/NyVHISM3JyVdIK8pMzUspVgQA +format=gzip: H4sIAAAAAAADAwtPzclRyEjNyclXSCvKTM1LKVYEAHN0w4sTAAAA diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index a97c7ec63d8..5d98e0cd027 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -63,6 +63,7 @@ CloseEvent CloseWatcher Comment CompositionEvent +CompressionStream CountQueuingStrategy Crypto CryptoKey diff --git a/Tests/LibWeb/Text/input/Compression/CompressionStream.html b/Tests/LibWeb/Text/input/Compression/CompressionStream.html new file mode 100644 index 00000000000..a0ffc4fa077 --- /dev/null +++ b/Tests/LibWeb/Text/input/Compression/CompressionStream.html @@ -0,0 +1,31 @@ + +