Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibWeb: Add CompressionStream/DecompressionStream #1379

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,6 +57,7 @@ ClipboardEvent
CloseEvent
CloseWatcher
Comment
CompressionStream
CountQueuingStrategy
Crypto
CryptoKey
Expand All @@ -81,6 +82,7 @@ DataTransferItem
DataTransferItemList
DataView
Date
DecompressionStream
DisposableStack
Document
DocumentFragment
Expand Down
16 changes: 16 additions & 0 deletions Userland/Libraries/LibCompress/Zlib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,20 @@ ErrorOr<ByteBuffer> ZlibCompressor::compress_all(ReadonlyBytes bytes, ZlibCompre
return buffer;
}

ErrorOr<ByteBuffer> ZlibDecompressor::decompress_all(ReadonlyBytes bytes)
{
// Even though the content encoding is "deflate", it's actually deflate with the zlib wrapper.
// https://tools.ietf.org/html/rfc7230#section-4.2.2
auto memory_stream = TRY(try_make<FixedMemoryStream>(bytes));
auto zlib_decompressor = ZlibDecompressor::create(move(memory_stream));
if (zlib_decompressor.is_error()) {
// From the RFC:
// "Note: Some non-conformant implementations send the "deflate"
// compressed data without the zlib wrapper."
return DeflateDecompressor::decompress_all(bytes);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else block here is unnecessary and I believe the linter usually complains when it sees that but I don't remember.

return zlib_decompressor.value()->read_until_eof();
}
}

}
3 changes: 2 additions & 1 deletion Userland/Libraries/LibCompress/Zlib.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class ZlibDecompressor : public Stream {
virtual bool is_open() const override;
virtual void close() override;

static ErrorOr<ByteBuffer> decompress_all(ReadonlyBytes);

private:
ZlibDecompressor(ZlibHeader, NonnullOwnPtr<Stream>);

Expand All @@ -72,7 +74,6 @@ 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
119 changes: 119 additions & 0 deletions Userland/Libraries/LibWeb/Compression/CompressionStream.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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)
{
auto stream = realm.heap().allocate<CompressionStream>(realm, realm, format);

auto& vm = realm.vm();
auto* env = vm.variable_environment();
if (env) {
// FIXME: Make this private to the web execution context
auto& global_object = realm.global_object();
auto constructor_value_or_error = global_object.get("CompressionStream_constructor");
auto constructor_value = TRY(constructor_value_or_error);
if (constructor_value.is_empty() || constructor_value.is_undefined()) {
return WebIDL::SimpleException {
WebIDL::SimpleExceptionType::TypeError,
"CompressionStream constructor not found"sv
};
}

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)));
TRY(func.internal_call(stream->m_this_value, move(arguments_list)));
}
return stream;
}

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("/Users/johandahlin/dev/ladybird/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()))
{
}

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;
}
Loading
Loading