Skip to content

Commit

Permalink
LibWeb: Add CompressionStream/DecompressionStream
Browse files Browse the repository at this point in the history
TODO: Figure out how to make JS APIs private
TODO: Bundle JS inside the executable
TODO: Create a proper test case
TODO: Figure out how to run WPT
  • Loading branch information
jdahlin committed Sep 12, 2024
1 parent be51c8a commit 629ae46
Show file tree
Hide file tree
Showing 15 changed files with 438 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ static bool is_platform_object(Type const& type)
"CanvasPattern"sv,
"CanvasRenderingContext2D"sv,
"CloseWatcher"sv,
"CompressionStream"sv,
"CryptoKey"sv,
"DataTransfer"sv,
"DecompressionStream"sv,
"Document"sv,
"DocumentType"sv,
"DOMRectReadOnly"sv,
Expand Down Expand Up @@ -4182,6 +4184,7 @@ static void generate_using_namespace_definitions(SourceGenerator& generator)
// FIXME: This is a total hack until we can figure out the namespace for a given type somehow.
using namespace Web::Animations;
using namespace Web::Clipboard;
using namespace Web::Compression;
using namespace Web::Crypto;
using namespace Web::CSS;
using namespace Web::DOM;
Expand Down
2 changes: 2 additions & 0 deletions Tests/LibWeb/Text/expected/all-window-properties.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ ClipboardEvent
CloseEvent
CloseWatcher
Comment
CompressionStream
CountQueuingStrategy
Crypto
CryptoKey
CustomElementRegistry
CustomEvent
DecompressionStream
DOMException
DOMImplementation
DOMMatrix
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibCompress/Zlib.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class ZlibCompressor : public Stream {
virtual bool is_open() const override;
virtual void close() override;
ErrorOr<void> finish();

static ErrorOr<ByteBuffer> compress_all(ReadonlyBytes bytes, ZlibCompressionLevel = ZlibCompressionLevel::Default);

private:
Expand Down
4 changes: 3 additions & 1 deletion Userland/Libraries/LibWeb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ set(SOURCES
Bindings/PlatformObject.cpp
Clipboard/Clipboard.cpp
Clipboard/ClipboardEvent.cpp
Compression/CompressionStream.cpp
Compression/DecompressionStream.cpp
Crypto/Crypto.cpp
Crypto/CryptoAlgorithms.cpp
Crypto/CryptoBindings.cpp
Expand Down Expand Up @@ -794,7 +796,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 skia)
target_link_libraries(LibWeb PRIVATE LibCore LibCompress LibCrypto LibJS LibHTTP LibGfx LibIPC LibRegex LibSyntax LibTextCodec LibUnicode LibMedia LibWasm LibXML LibIDL LibURL LibTLS LibRequests skia)

generate_js_bindings(LibWeb)

Expand Down
108 changes: 108 additions & 0 deletions Userland/Libraries/LibWeb/Compression/CompressionStream.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2024, Johan Dahlin <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibCompress/Deflate.h>
#include <LibCompress/Gzip.h>
#include <LibCompress/Zlib.h>
#include <LibJS/Bytecode/Interpreter.h>
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Bindings/CompressionStreamPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Compression/CompressionStream.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Buffers.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::Compression {

JS_DEFINE_ALLOCATOR(CompressionStream);

WebIDL::ExceptionOr<JS::NonnullGCPtr<CompressionStream>> CompressionStream::construct_impl(JS::Realm& realm, Bindings::CompressionFormat format)
{
return realm.heap().allocate<CompressionStream>(realm, realm, format);
}

WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Uint8Array>> CompressionStream::compress(JS::VM& vm, Bindings::CompressionFormat format, JS::Handle<WebIDL::BufferSource> buffer_source)
{
auto realm = vm.current_realm();
auto data_buffer_or_error = WebIDL::get_buffer_source_copy(*buffer_source->raw_object());
if (data_buffer_or_error.is_error())
return WebIDL::OperationError::create(*realm, "Failed to copy bytes from ArrayBuffer"_fly_string);

ByteBuffer const& data_buffer = data_buffer_or_error.value();
Optional<ErrorOr<ByteBuffer>> compressed;

if (format == Bindings::CompressionFormat::Deflate) {
compressed = Compress::ZlibCompressor::compress_all(data_buffer);
} else if (format == Bindings::CompressionFormat::Gzip) {
compressed = Compress::GzipCompressor::compress_all(data_buffer);
} else if (format == Bindings::CompressionFormat::DeflateRaw) {
compressed = Compress::DeflateCompressor::compress_all(data_buffer);
} else {
return WebIDL::SimpleException {
WebIDL::SimpleExceptionType::TypeError,
"Invalid compression format"sv
};
}

if (compressed.value().is_error())
return WebIDL::OperationError::create(*realm, "Failed to compress data"_fly_string);

auto compressed_data = compressed.value().release_value();
auto array_buffer = JS::ArrayBuffer::create(*realm, compressed_data);
return JS::Uint8Array::create(*realm, array_buffer->byte_length(), *array_buffer);
}

static JS::GCPtr<JS::Script> import_js_script(JS::Realm& realm)
{
auto& vm = realm.vm();
auto file = MUST(Core::File::open("Userland/Libraries/LibWeb/Compression/CompressionStream.js"sv, Core::File::OpenMode::Read));
auto file_contents = MUST(file->read_until_eof());
auto source = StringView { file_contents };

auto script = MUST(JS::Script::parse(source, realm, "CompressionStream.js"sv));
MUST(vm.bytecode_interpreter().run(*script));
return script;
}

CompressionStream::CompressionStream(JS::Realm& realm, Bindings::CompressionFormat format)
: Bindings::PlatformObject(realm)
, m_format(format)
, m_js_script(import_js_script(realm))
, m_this_value(JS::Object::create(realm, realm.intrinsics().object_prototype()))
{
auto& vm = realm.vm();
auto* env = vm.variable_environment();
if (env) {
// FIXME: Make this private to the web execution context
auto constructor_value = MUST(env->get_binding_value(vm, "CompressionStream_constructor", true));
auto& func = static_cast<JS::ECMAScriptFunctionObject&>(constructor_value.as_function());
JS::MarkedVector<JS::Value> arguments_list { vm.heap() };
arguments_list.append(JS::PrimitiveString::create(vm, Bindings::idl_enum_to_string(format)));
MUST(func.internal_call(m_this_value, move(arguments_list)));
}
}

CompressionStream::~CompressionStream() = default;

void CompressionStream::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CompressionStream);
}

void CompressionStream::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_js_script);
visitor.visit(m_this_value);
}

}
50 changes: 50 additions & 0 deletions Userland/Libraries/LibWeb/Compression/CompressionStream.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024, Johan Dahlin <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#pragma once

#include <LibJS/Script.h>
#include <LibWeb/Bindings/CompressionStreamPrototype.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Forward.h>
#include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/Streams/WritableStream.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::Compression {

class CompressionStream final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(CompressionStream, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CompressionStream);

public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CompressionStream>> construct_impl(JS::Realm&, Bindings::CompressionFormat format);
static WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Uint8Array>> compress(JS::VM& vm, Bindings::CompressionFormat format, JS::Handle<WebIDL::BufferSource> buffer_source);
virtual ~CompressionStream() override;

JS::GCPtr<Web::Streams::ReadableStream> readable() const
{
auto readable = MUST(m_this_value->get(JS::PropertyKey { "readable" }));
return verify_cast<Web::Streams::ReadableStream>(readable.as_object());
}

JS::GCPtr<Web::Streams::WritableStream> writable() const
{
auto writable = MUST(m_this_value->get(JS::PropertyKey { "writable" }));
return verify_cast<Web::Streams::WritableStream>(writable.as_object());
}

private:
CompressionStream(JS::Realm&, Bindings::CompressionFormat);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;

Bindings::CompressionFormat m_format;
JS::GCPtr<JS::Script> m_js_script;
JS::GCPtr<JS::Object> m_this_value;
};

}
18 changes: 18 additions & 0 deletions Userland/Libraries/LibWeb/Compression/CompressionStream.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// https://streams.spec.whatwg.org/#generictransformstream
interface mixin GenericTransformStream {
readonly attribute ReadableStream readable;
readonly attribute WritableStream writable;
};

// https://compression.spec.whatwg.org/#compression-stream
enum CompressionFormat { "deflate", "deflate-raw", "gzip" };

[Exposed=*]
interface CompressionStream {
constructor(CompressionFormat format);

// Non-standard
static Uint8Array compress(CompressionFormat format, BufferSource data);

};
CompressionStream includes GenericTransformStream;
20 changes: 20 additions & 0 deletions Userland/Libraries/LibWeb/Compression/CompressionStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function CompressionStream_constructor(format) {
this.transform = new TransformStream({
start(controller) {
controller.temp = [];
return Promise.resolve();
},
transform(chunk, controller) {
controller.temp.push(CompressionStream.compress(format, chunk));
return Promise.resolve();
},
flush(controller) {
for (chunk of controller.temp) {
controller.enqueue(chunk);
}
return Promise.resolve();
},
});
this.readable = this.transform.readable;
this.writable = this.transform.writable;
}
110 changes: 110 additions & 0 deletions Userland/Libraries/LibWeb/Compression/DecompressionStream.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2024, Johan Dahlin <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibCompress/Deflate.h>
#include <LibCompress/Gzip.h>
#include <LibCompress/Zlib.h>
#include <LibJS/Bytecode/Interpreter.h>
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Bindings/DecompressionStreamPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Compression/DecompressionStream.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Buffers.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::Compression {

JS_DEFINE_ALLOCATOR(DecompressionStream);

WebIDL::ExceptionOr<JS::NonnullGCPtr<DecompressionStream>> DecompressionStream::construct_impl(JS::Realm& realm, Bindings::CompressionFormat format)
{
return realm.heap().allocate<DecompressionStream>(realm, realm, format);
}

WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Uint8Array>> DecompressionStream::decompress(JS::VM& vm, Bindings::CompressionFormat format, JS::Handle<WebIDL::BufferSource> buffer_source)
{
auto realm = vm.current_realm();
auto data_buffer_or_error = WebIDL::get_buffer_source_copy(*buffer_source->raw_object());
if (data_buffer_or_error.is_error())
return WebIDL::OperationError::create(*realm, "Failed to copy bytes from ArrayBuffer"_fly_string);

ByteBuffer const& data_buffer = data_buffer_or_error.value();
Optional<ErrorOr<ByteBuffer>> decompressed;

if (format == Bindings::CompressionFormat::Deflate) {
decompressed = Compress::ZlibDecompressor::decompress_all(data_buffer);
} else if (format == Bindings::CompressionFormat::Gzip) {
decompressed = Compress::GzipDecompressor::decompress_all(data_buffer);
} else if (format == Bindings::CompressionFormat::DeflateRaw) {
decompressed = Compress::DeflateDecompressor::decompress_all(data_buffer);
} else {
return WebIDL::SimpleException {
WebIDL::SimpleExceptionType::TypeError,
"Invalid compression format"sv
};
}

if (decompressed.value().is_error()) {
outln("Failed to decompress data: {}", decompressed.value().error());
return WebIDL::OperationError::create(*realm, "Failed to decompress data"_fly_string);
}

auto decompressed_data = decompressed.value().release_value();
auto array_buffer = JS::ArrayBuffer::create(*realm, decompressed_data);
return JS::Uint8Array::create(*realm, array_buffer->byte_length(), *array_buffer);
}

static JS::GCPtr<JS::Script> import_js_script(JS::Realm& realm)
{
auto& vm = realm.vm();
auto file = MUST(Core::File::open("Userland/Libraries/LibWeb/Compression/DecompressionStream.js"sv, Core::File::OpenMode::Read));
auto file_contents = MUST(file->read_until_eof());
auto source = StringView { file_contents };

auto script = MUST(JS::Script::parse(source, realm, "DecompressionStream.js"sv));
MUST(vm.bytecode_interpreter().run(*script));
return script;
}

DecompressionStream::DecompressionStream(JS::Realm& realm, Bindings::CompressionFormat format)
: Bindings::PlatformObject(realm)
, m_format(format)
, m_js_script(import_js_script(realm))
, m_this_value(JS::Object::create(realm, realm.intrinsics().object_prototype()))
{
auto& vm = realm.vm();
auto* env = vm.variable_environment();
if (env) {
// FIXME: Make this private to the web execution context
auto constructor_value = MUST(env->get_binding_value(vm, "DecompressionStream_constructor", true));
auto& func = static_cast<JS::ECMAScriptFunctionObject&>(constructor_value.as_function());
JS::MarkedVector<JS::Value> arguments_list { vm.heap() };
arguments_list.append(JS::PrimitiveString::create(vm, Bindings::idl_enum_to_string(format)));
MUST(func.internal_call(m_this_value, move(arguments_list)));
}
}

DecompressionStream::~DecompressionStream() = default;

void DecompressionStream::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(DecompressionStream);
}

void DecompressionStream::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_js_script);
visitor.visit(m_this_value);
}

}
Loading

0 comments on commit 629ae46

Please sign in to comment.