diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 20177c7d8b88..511718e5fd64 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -244,6 +244,7 @@ set(SOURCES Encoding/TextDecoder.cpp Encoding/TextEncoder.cpp Encoding/TextEncoderCommon.cpp + Encoding/TextEncoderStream.cpp EntriesAPI/FileSystemEntry.cpp EventTiming/PerformanceEventTiming.cpp Fetch/Body.cpp diff --git a/Libraries/LibWeb/Encoding/TextEncoderStream.cpp b/Libraries/LibWeb/Encoding/TextEncoderStream.cpp new file mode 100644 index 000000000000..4283e6219af1 --- /dev/null +++ b/Libraries/LibWeb/Encoding/TextEncoderStream.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2025, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Encoding { + +GC_DEFINE_ALLOCATOR(TextEncoderStream); + +// https://encoding.spec.whatwg.org/#dom-textencoderstream +WebIDL::ExceptionOr> TextEncoderStream::construct_impl(JS::Realm& realm) +{ + // 1. Set this’s encoder to an instance of the UTF-8 encoder. + // NOTE: No-op, as AK::String is already in UTF-8 format. + + // NOTE: We do these steps first so that we may store it as nonnull in the GenericTransformStream. + // 4. Let transformStream be a new TransformStream. + auto transform_stream = realm.create(realm); + + // 6. Set this's transform to a new TransformStream. + auto stream = realm.create(realm, transform_stream); + + // 2. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the encode 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->encode_and_enqueue_chunk(chunk); result.is_error()) { + auto throw_completion = Bindings::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()); + }); + + // 3. Let flushAlgorithm be an algorithm which runs the encode and flush 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->encode_and_flush(); result.is_error()) { + auto throw_completion = Bindings::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()); + }); + + // 5. Set up transformStream with transformAlgorithm set to transformAlgorithm and flushAlgorithm set to flushAlgorithm. + transform_stream->set_up(transform_algorithm, flush_algorithm); + + return stream; +} + +TextEncoderStream::TextEncoderStream(JS::Realm& realm, GC::Ref transform) + : Bindings::PlatformObject(realm) + , Streams::GenericTransformStreamMixin(transform) +{ +} + +TextEncoderStream::~TextEncoderStream() = default; + +void TextEncoderStream::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(TextEncoderStream); +} + +void TextEncoderStream::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + Streams::GenericTransformStreamMixin::visit_edges(visitor); +} + +// https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk +WebIDL::ExceptionOr TextEncoderStream::encode_and_enqueue_chunk(JS::Value chunk) +{ + // Spec Note: This is equivalent to the "convert a string into a scalar value string" algorithm from the Infra + // Standard, but allows for surrogate pairs that are split between strings. [INFRA] + + auto& realm = this->realm(); + auto& vm = this->vm(); + + // 1. Let input be the result of converting chunk to a DOMString. + auto input = TRY(chunk.to_string(vm)); + + // 2. Convert input to an I/O queue of code units. + // Spec Note: DOMString, as well as an I/O queue of code units rather than scalar values, are used here so that a + // surrogate pair that is split between chunks can be reassembled into the appropriate scalar value. + // The behavior is otherwise identical to USVString. In particular, lone surrogates will be replaced + // with U+FFFD. + auto code_points = input.code_points(); + auto it = code_points.begin(); + + // 3. Let output be the I/O queue of bytes « end-of-queue ». + ByteBuffer output; + + // 4. While true: + while (true) { + // 2. If item is end-of-queue, then: + // NOTE: This is done out-of-order so that we're not dereferencing a code point iterator that points to the end. + if (it.done()) { + // 1. Convert output into a byte sequence. + // Note: No-op. + + // 2. If output is non-empty, then: + if (!output.is_empty()) { + // 1. Let chunk be a Uint8Array object wrapping an ArrayBuffer containing output. + auto array_buffer = JS::ArrayBuffer::create(realm, move(output)); + auto array = JS::Uint8Array::create(realm, array_buffer->byte_length(), *array_buffer); + + // 2. Enqueue chunk into encoder’s transform. + TRY(Streams::transform_stream_default_controller_enqueue(*m_transform->controller(), array)); + } + + // 3. Return. + return {}; + } + + // 1. Let item be the result of reading from input. + auto item = *it; + + // 3. Let result be the result of executing the convert code unit to scalar value algorithm with encoder, item and input. + auto result = convert_code_unit_to_scalar_value(item, it); + + // 4. If result is not continue, then process an item with result, encoder’s encoder, input, output, and "fatal". + if (result.has_value()) { + (void)AK::UnicodeUtils::code_point_to_utf8(result.value(), [&output](char utf8_byte) { + output.append(static_cast(utf8_byte)); + }); + } + } +} + +// https://encoding.spec.whatwg.org/#encode-and-flush +WebIDL::ExceptionOr TextEncoderStream::encode_and_flush() +{ + auto& realm = this->realm(); + + // 1. If encoder’s leading surrogate is non-null, then: + if (m_leading_surrogate.has_value()) { + // 1. Let chunk be a Uint8Array object wrapping an ArrayBuffer containing 0xEF 0xBF 0xBD. + // Spec Note: This is U+FFFD (�) in UTF-8 bytes. + constexpr static u8 replacement_character_utf8_bytes[3] = { 0xEF, 0xBF, 0xBD }; + auto bytes = MUST(ByteBuffer::copy(replacement_character_utf8_bytes, sizeof(replacement_character_utf8_bytes))); + auto array_buffer = JS::ArrayBuffer::create(realm, bytes); + auto chunk = JS::Uint8Array::create(realm, array_buffer->byte_length(), *array_buffer); + + // 2. Enqueue chunk into encoder’s transform. + TRY(Streams::transform_stream_default_controller_enqueue(*m_transform->controller(), chunk)); + } + + return {}; +} + +// https://encoding.spec.whatwg.org/#convert-code-unit-to-scalar-value +Optional TextEncoderStream::convert_code_unit_to_scalar_value(u32 item, Utf8CodePointIterator& code_point_iterator) +{ + ArmedScopeGuard move_to_next_code_point_guard = [&] { + ++code_point_iterator; + }; + + // 1. If encoder’s leading surrogate is non-null, then: + if (m_leading_surrogate.has_value()) { + // 1. Let leadingSurrogate be encoder’s leading surrogate. + auto leading_surrogate = m_leading_surrogate.value(); + + // 2. Set encoder’s leading surrogate to null. + m_leading_surrogate.clear(); + + // 3. If item is a trailing surrogate, then return a scalar value from surrogates given leadingSurrogate + // and item. + if (Utf16View::is_low_surrogate(item)) { + // https://encoding.spec.whatwg.org/#scalar-value-from-surrogates + // To obtain a scalar value from surrogates, given a leading surrogate leading and a trailing surrogate + // trailing, return 0x10000 + ((leading − 0xD800) << 10) + (trailing − 0xDC00). + return Utf16View::decode_surrogate_pair(leading_surrogate, item); + } + + // 4. Restore item to input. + move_to_next_code_point_guard.disarm(); + + // 5. Return U+FFFD. + return 0xFFFD; + } + + // 2. If item is a leading surrogate, then set encoder’s leading surrogate to item and return continue. + if (Utf16View::is_high_surrogate(item)) { + m_leading_surrogate = item; + return OptionalNone {}; + } + + // 3. If item is a trailing surrogate, then return U+FFFD. + if (Utf16View::is_low_surrogate(item)) + return 0xFFFD; + + // 4. Return item. + return item; +} + +} diff --git a/Libraries/LibWeb/Encoding/TextEncoderStream.h b/Libraries/LibWeb/Encoding/TextEncoderStream.h new file mode 100644 index 000000000000..7298feca1c51 --- /dev/null +++ b/Libraries/LibWeb/Encoding/TextEncoderStream.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::Encoding { + +class TextEncoderStream final + : public Bindings::PlatformObject + , public Streams::GenericTransformStreamMixin + , public TextEncoderCommonMixin { + WEB_PLATFORM_OBJECT(TextEncoderStream, Bindings::PlatformObject); + GC_DECLARE_ALLOCATOR(TextEncoderStream); + +public: + static WebIDL::ExceptionOr> construct_impl(JS::Realm&); + virtual ~TextEncoderStream() override; + +private: + TextEncoderStream(JS::Realm&, GC::Ref); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + WebIDL::ExceptionOr encode_and_enqueue_chunk(JS::Value); + WebIDL::ExceptionOr encode_and_flush(); + + Optional convert_code_unit_to_scalar_value(u32 item, Utf8CodePointIterator& code_point_iterator); + + // https://encoding.spec.whatwg.org/#textencoderstream-pending-high-surrogate + Optional m_leading_surrogate; +}; + +} diff --git a/Libraries/LibWeb/Encoding/TextEncoderStream.idl b/Libraries/LibWeb/Encoding/TextEncoderStream.idl new file mode 100644 index 000000000000..160d21a2419f --- /dev/null +++ b/Libraries/LibWeb/Encoding/TextEncoderStream.idl @@ -0,0 +1,11 @@ +#import +#import + +// https://encoding.spec.whatwg.org/#textencoderstream +[Exposed=*] +interface TextEncoderStream { + constructor(); +}; + +TextEncoderStream includes TextEncoderCommon; +TextEncoderStream includes GenericTransformStream; diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 08761758e0cb..0ec9512e140d 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -327,6 +327,7 @@ class XMLSerializer; namespace Web::Encoding { class TextDecoder; class TextEncoder; +class TextEncoderStream; struct TextDecodeOptions; struct TextDecoderOptions; diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index da7a42fd6235..371519362251 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -88,6 +88,7 @@ libweb_js_bindings(DOMURL/DOMURL) libweb_js_bindings(DOMURL/URLSearchParams ITERABLE) libweb_js_bindings(Encoding/TextDecoder) libweb_js_bindings(Encoding/TextEncoder) +libweb_js_bindings(Encoding/TextEncoderStream) libweb_js_bindings(EntriesAPI/FileSystemEntry) libweb_js_bindings(EventTiming/PerformanceEventTiming) libweb_js_bindings(Fetch/Headers ITERABLE) diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 98863bb8765a..9eec691f26b3 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -381,6 +381,7 @@ SyntaxError Text TextDecoder TextEncoder +TextEncoderStream TextEvent TextMetrics TextTrack diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-shadowrealm.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-shadowrealm.txt new file mode 100644 index 000000000000..6055c864758f --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-shadowrealm.txt @@ -0,0 +1,3 @@ +Harness status: Error + +Found 0 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-window.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-window.txt new file mode 100644 index 000000000000..6055c864758f --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-window.txt @@ -0,0 +1,3 @@ +Harness status: Error + +Found 0 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.txt new file mode 100644 index 000000000000..271514813113 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/backpressure.any.txt @@ -0,0 +1,10 @@ +Harness status: OK + +Found 4 tests + +2 Pass +2 Fail +Fail write() should not complete until read relieves backpressure for TextDecoderStream +Fail additional writes should wait for backpressure to be relieved for class TextDecoderStream +Pass write() should not complete until read relieves backpressure for TextEncoderStream +Pass additional writes should wait for backpressure to be relieved for class TextEncoderStream \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt new file mode 100644 index 000000000000..db36b3a6af60 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt @@ -0,0 +1,12 @@ +Harness status: Error + +Found 6 tests + +1 Pass +5 Fail +Pass a chunk that cannot be converted to a string should error the streams +Fail input of type undefined should be converted correctly to string +Fail input of type null should be converted correctly to string +Fail input of type numeric should be converted correctly to string +Fail input of type object should be converted correctly to string +Fail input of type array should be converted correctly to string \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-utf8.any.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-utf8.any.txt new file mode 100644 index 000000000000..67e9e3a3def8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-utf8.any.txt @@ -0,0 +1,24 @@ +Harness status: OK + +Found 19 tests + +19 Pass +Pass encoding one string of UTF-8 should give one complete chunk +Pass a character split between chunks should be correctly encoded +Pass a character following one split between chunks should be correctly encoded +Pass two consecutive astral characters each split down the middle should be correctly reassembled +Pass two consecutive astral characters each split down the middle with an invalid surrogate in the middle should be correctly encoded +Pass a stream ending in a leading surrogate should emit a replacement character as a final chunk +Pass an unmatched surrogate at the end of a chunk followed by an astral character in the next chunk should be replaced with the replacement character at the start of the next output chunk +Pass an unmatched surrogate at the end of a chunk followed by an ascii character in the next chunk should be replaced with the replacement character at the start of the next output chunk +Pass an unmatched surrogate at the end of a chunk followed by a plane 1 character split into two chunks should result in the encoded plane 1 character appearing in the last output chunk +Pass two leading chunks should result in two replacement characters +Pass a non-terminal unpaired leading surrogate should immediately be replaced +Pass a terminal unpaired trailing surrogate should immediately be replaced +Pass a leading surrogate chunk should be carried past empty chunks +Pass a leading surrogate chunk should error when it is clear it didn't form a pair +Pass an empty string should result in no output chunk +Pass a leading empty chunk should be ignored +Pass a trailing empty chunk should be ignored +Pass a plain ASCII chunk should be converted +Pass characters in the ISO-8859-1 range should be encoded correctly \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/invalid-realm.window.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/invalid-realm.window.txt new file mode 100644 index 000000000000..dc438b19908c --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/invalid-realm.window.txt @@ -0,0 +1,10 @@ +Harness status: OK + +Found 4 tests + +2 Pass +2 Fail +Fail TextDecoderStream: write in detached realm should succeed +Pass TextEncoderStream: write in detached realm should succeed +Pass TextEncoderStream: close in detached realm should succeed +Fail TextDecoderStream: close in detached realm should succeed \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-shadowrealm.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-shadowrealm.txt new file mode 100644 index 000000000000..6055c864758f --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-shadowrealm.txt @@ -0,0 +1,3 @@ +Harness status: Error + +Found 0 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-window.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-window.txt new file mode 100644 index 000000000000..6055c864758f --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-window.txt @@ -0,0 +1,3 @@ +Harness status: Error + +Found 0 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.txt new file mode 100644 index 000000000000..d7e6d6183081 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/readable-writable-properties.any.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 2 tests + +1 Pass +1 Fail +Pass TextEncoderStream readable and writable properties must pass brand checks +Fail TextDecoderStream readable and writable properties must pass brand checks \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/realms.window.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/realms.window.txt new file mode 100644 index 000000000000..80ef712649f6 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/realms.window.txt @@ -0,0 +1,18 @@ +Harness status: Error + +Found 12 tests + +2 Pass +10 Fail +Pass a TextEncoderStream object should be associated with the realm the constructor came from +Pass TextEncoderStream's readable and writable attributes should come from the same realm as the constructor definition +Fail the output chunks when read is called after write should come from the same realm as the constructor of TextEncoderStream +Fail the output chunks when write is called with a pending read should come from the same realm as the constructor of TextEncoderStream +Fail TypeError for unconvertable chunk should come from constructor realm of TextEncoderStream +Fail a TextDecoderStream object should be associated with the realm the constructor came from +Fail TextDecoderStream's readable and writable attributes should come from the same realm as the constructor definition +Fail the result object when read is called after write should come from the same realm as the constructor of TextDecoderStream +Fail the result object when write is called with a pending read should come from the same realm as the constructor of TextDecoderStream +Fail TypeError for chunk with the wrong type should come from constructor realm of TextDecoderStream +Fail TypeError for invalid chunk should come from constructor realm of TextDecoderStream +Fail TypeError for incomplete input should come from constructor realm of TextDecoderStream \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.html new file mode 100644 index 000000000000..b3ce101782c0 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.js b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.js new file mode 100644 index 000000000000..0f67e3358414 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.js @@ -0,0 +1,60 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const classes = [ + { + name: 'TextDecoderStream', + input: new Uint8Array([65]) + }, + { + name: 'TextEncoderStream', + input: 'A' + } +]; + +const microtasksRun = () => new Promise(resolve => step_timeout(resolve, 0)); + +for (const streamClass of classes) { + promise_test(async () => { + const stream = new self[streamClass.name](); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + const events = []; + await microtasksRun(); + const writePromise = writer.write(streamClass.input); + writePromise.then(() => events.push('write')); + await microtasksRun(); + events.push('paused'); + await reader.read(); + events.push('read'); + await writePromise; + assert_array_equals(events, ['paused', 'read', 'write'], + 'write should happen after read'); + }, 'write() should not complete until read relieves backpressure for ' + + `${streamClass.name}`); + + promise_test(async () => { + const stream = new self[streamClass.name](); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + const events = []; + await microtasksRun(); + const readPromise1 = reader.read(); + readPromise1.then(() => events.push('read1')); + const writePromise1 = writer.write(streamClass.input); + const writePromise2 = writer.write(streamClass.input); + writePromise1.then(() => events.push('write1')); + writePromise2.then(() => events.push('write2')); + await microtasksRun(); + events.push('paused'); + const readPromise2 = reader.read(); + readPromise2.then(() => events.push('read2')); + await Promise.all([writePromise1, writePromise2, + readPromise1, readPromise2]); + assert_array_equals(events, ['read1', 'write1', 'paused', 'read2', + 'write2'], + 'writes should not happen before read2'); + }, 'additional writes should wait for backpressure to be relieved for ' + + `class ${streamClass.name}`); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-shadowrealm.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-shadowrealm.html new file mode 100644 index 000000000000..041066b363e0 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-shadowrealm.html @@ -0,0 +1,40 @@ + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-window.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-window.html new file mode 100644 index 000000000000..6b22f5fb3b47 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/backpressure.any.shadowrealm-in-window.html @@ -0,0 +1,24 @@ + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-bad-chunks.any.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-bad-chunks.any.html new file mode 100644 index 000000000000..2b50afd4d912 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-bad-chunks.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-bad-chunks.any.js b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-bad-chunks.any.js new file mode 100644 index 000000000000..4a926a67d287 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-bad-chunks.any.js @@ -0,0 +1,63 @@ +// META: global=window,worker +// META: script=resources/readable-stream-from-array.js +// META: script=resources/readable-stream-to-array.js + +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +promise_test(t => { + const ts = new TextEncoderStream(); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + const writePromise = writer.write({ + toString() { throw error1; } + }); + const readPromise = reader.read(); + return Promise.all([ + promise_rejects_exactly(t, error1, readPromise, 'read should reject with error1'), + promise_rejects_exactly(t, error1, writePromise, 'write should reject with error1'), + promise_rejects_exactly(t, error1, reader.closed, 'readable should be errored with error1'), + promise_rejects_exactly(t, error1, writer.closed, 'writable should be errored with error1'), + ]); +}, 'a chunk that cannot be converted to a string should error the streams'); + +const oddInputs = [ + { + name: 'undefined', + value: undefined, + expected: 'undefined' + }, + { + name: 'null', + value: null, + expected: 'null' + }, + { + name: 'numeric', + value: 3.14, + expected: '3.14' + }, + { + name: 'object', + value: {}, + expected: '[object Object]' + }, + { + name: 'array', + value: ['hi'], + expected: 'hi' + } +]; + +for (const input of oddInputs) { + promise_test(async () => { + const outputReadable = readableStreamFromArray([input.value]) + .pipeThrough(new TextEncoderStream()) + .pipeThrough(new TextDecoderStream()); + const output = await readableStreamToArray(outputReadable); + assert_equals(output.length, 1, 'output should contain one chunk'); + assert_equals(output[0], input.expected, 'output should be correct'); + }, `input of type ${input.name} should be converted correctly to string`); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-utf8.any.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-utf8.any.html new file mode 100644 index 000000000000..29e3d1fdc559 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-utf8.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-utf8.any.js b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-utf8.any.js new file mode 100644 index 000000000000..a5ba8f91eaf7 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/encode-utf8.any.js @@ -0,0 +1,144 @@ +// META: global=window,worker +// META: script=resources/readable-stream-from-array.js +// META: script=resources/readable-stream-to-array.js + +'use strict'; +const inputString = 'I \u{1F499} streams'; +const expectedOutputBytes = [0x49, 0x20, 0xf0, 0x9f, 0x92, 0x99, 0x20, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73]; +// This is a character that must be represented in two code units in a string, +// ie. it is not in the Basic Multilingual Plane. +const astralCharacter = '\u{1F499}'; // BLUE HEART +const astralCharacterEncoded = [0xf0, 0x9f, 0x92, 0x99]; +const leading = astralCharacter[0]; +const trailing = astralCharacter[1]; +const replacementEncoded = [0xef, 0xbf, 0xbd]; + +// These tests assume that the implementation correctly classifies leading and +// trailing surrogates and treats all the code units in each set equivalently. + +const testCases = [ + { + input: [inputString], + output: [expectedOutputBytes], + description: 'encoding one string of UTF-8 should give one complete chunk' + }, + { + input: [leading, trailing], + output: [astralCharacterEncoded], + description: 'a character split between chunks should be correctly encoded' + }, + { + input: [leading, trailing + astralCharacter], + output: [astralCharacterEncoded.concat(astralCharacterEncoded)], + description: 'a character following one split between chunks should be ' + + 'correctly encoded' + }, + { + input: [leading, trailing + leading, trailing], + output: [astralCharacterEncoded, astralCharacterEncoded], + description: 'two consecutive astral characters each split down the ' + + 'middle should be correctly reassembled' + }, + { + input: [leading, trailing + leading + leading, trailing], + output: [astralCharacterEncoded.concat(replacementEncoded), astralCharacterEncoded], + description: 'two consecutive astral characters each split down the ' + + 'middle with an invalid surrogate in the middle should be correctly ' + + 'encoded' + }, + { + input: [leading], + output: [replacementEncoded], + description: 'a stream ending in a leading surrogate should emit a ' + + 'replacement character as a final chunk' + }, + { + input: [leading, astralCharacter], + output: [replacementEncoded.concat(astralCharacterEncoded)], + description: 'an unmatched surrogate at the end of a chunk followed by ' + + 'an astral character in the next chunk should be replaced with ' + + 'the replacement character at the start of the next output chunk' + }, + { + input: [leading, 'A'], + output: [replacementEncoded.concat([65])], + description: 'an unmatched surrogate at the end of a chunk followed by ' + + 'an ascii character in the next chunk should be replaced with ' + + 'the replacement character at the start of the next output chunk' + }, + { + input: [leading, leading, trailing], + output: [replacementEncoded, astralCharacterEncoded], + description: 'an unmatched surrogate at the end of a chunk followed by ' + + 'a plane 1 character split into two chunks should result in ' + + 'the encoded plane 1 character appearing in the last output chunk' + }, + { + input: [leading, leading], + output: [replacementEncoded, replacementEncoded], + description: 'two leading chunks should result in two replacement ' + + 'characters' + }, + { + input: [leading + leading, trailing], + output: [replacementEncoded, astralCharacterEncoded], + description: 'a non-terminal unpaired leading surrogate should ' + + 'immediately be replaced' + }, + { + input: [trailing, astralCharacter], + output: [replacementEncoded, astralCharacterEncoded], + description: 'a terminal unpaired trailing surrogate should ' + + 'immediately be replaced' + }, + { + input: [leading, '', trailing], + output: [astralCharacterEncoded], + description: 'a leading surrogate chunk should be carried past empty chunks' + }, + { + input: [leading, ''], + output: [replacementEncoded], + description: 'a leading surrogate chunk should error when it is clear ' + + 'it didn\'t form a pair' + }, + { + input: [''], + output: [], + description: 'an empty string should result in no output chunk' + }, + { + input: ['', inputString], + output: [expectedOutputBytes], + description: 'a leading empty chunk should be ignored' + }, + { + input: [inputString, ''], + output: [expectedOutputBytes], + description: 'a trailing empty chunk should be ignored' + }, + { + input: ['A'], + output: [[65]], + description: 'a plain ASCII chunk should be converted' + }, + { + input: ['\xff'], + output: [[195, 191]], + description: 'characters in the ISO-8859-1 range should be encoded correctly' + }, +]; + +for (const {input, output, description} of testCases) { + promise_test(async () => { + const inputStream = readableStreamFromArray(input); + const outputStream = inputStream.pipeThrough(new TextEncoderStream()); + const chunkArray = await readableStreamToArray(outputStream); + assert_equals(chunkArray.length, output.length, + 'number of chunks should match'); + for (let i = 0; i < output.length; ++i) { + assert_array_equals(chunkArray[i], output[i], `chunk ${i} should match`); + } + }, description); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/invalid-realm.window.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/invalid-realm.window.html new file mode 100644 index 000000000000..58c9ff971313 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/invalid-realm.window.html @@ -0,0 +1,8 @@ + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/invalid-realm.window.js b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/invalid-realm.window.js new file mode 100644 index 000000000000..beaec42641fe --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/invalid-realm.window.js @@ -0,0 +1,37 @@ +// Text*Stream should still work even if the realm is detached. + +// Adds an iframe to the document and returns it. +function addIframe() { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + return iframe; +} + +promise_test(async t => { + const iframe = addIframe(); + const stream = new iframe.contentWindow.TextDecoderStream(); + const readPromise = stream.readable.getReader().read(); + const writer = stream.writable.getWriter(); + await writer.ready; + iframe.remove(); + return Promise.all([writer.write(new Uint8Array([65])),readPromise]); +}, 'TextDecoderStream: write in detached realm should succeed'); + +promise_test(async t => { + const iframe = addIframe(); + const stream = new iframe.contentWindow.TextEncoderStream(); + const readPromise = stream.readable.getReader().read(); + const writer = stream.writable.getWriter(); + await writer.ready; + iframe.remove(); + return Promise.all([writer.write('A'), readPromise]); +}, 'TextEncoderStream: write in detached realm should succeed'); + +for (const type of ['TextEncoderStream', 'TextDecoderStream']) { + promise_test(async t => { + const iframe = addIframe(); + const stream = new iframe.contentWindow[type](); + iframe.remove(); + return stream.writable.close(); + }, `${type}: close in detached realm should succeed`); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.html new file mode 100644 index 000000000000..44a839677688 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.js b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.js new file mode 100644 index 000000000000..8084cb994214 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker,shadowrealm + +// This just tests that the "readable" and "writable" properties pass the brand +// checks. All other relevant attributes are covered by the IDL tests. + +'use strict'; + +test(() => { + const te = new TextEncoderStream(); + assert_equals(typeof ReadableStream.prototype.getReader.call(te.readable), + 'object', 'readable property must pass brand check'); + assert_equals(typeof WritableStream.prototype.getWriter.call(te.writable), + 'object', 'writable property must pass brand check'); +}, 'TextEncoderStream readable and writable properties must pass brand checks'); + +test(() => { + const td = new TextDecoderStream(); + assert_equals(typeof ReadableStream.prototype.getReader.call(td.readable), + 'object', 'readable property must pass brand check'); + assert_equals(typeof WritableStream.prototype.getWriter.call(td.writable), + 'object', 'writable property must pass brand check'); +}, 'TextDecoderStream readable and writable properties must pass brand checks'); diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-shadowrealm.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-shadowrealm.html new file mode 100644 index 000000000000..1e72154db099 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-shadowrealm.html @@ -0,0 +1,40 @@ + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-window.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-window.html new file mode 100644 index 000000000000..b8ecfb5690d8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/readable-writable-properties.any.shadowrealm-in-window.html @@ -0,0 +1,24 @@ + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/realms.window.html b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/realms.window.html new file mode 100644 index 000000000000..4098f1c4be1d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/realms.window.html @@ -0,0 +1,8 @@ + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/encoding/streams/realms.window.js b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/realms.window.js new file mode 100644 index 000000000000..ca9ce21abc22 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/encoding/streams/realms.window.js @@ -0,0 +1,304 @@ +'use strict'; + +// Test that objects created by the TextEncoderStream and TextDecoderStream APIs +// are created in the correct realm. The tests work by creating an iframe for +// each realm and then posting Javascript to them to be evaluated. Inputs and +// outputs are passed around via global variables in each realm's scope. + +// Async setup is required before creating any tests, so require done() to be +// called. +setup({explicit_done: true}); + +function createRealm() { + let iframe = document.createElement('iframe'); + const scriptEndTag = '<' + '/script>'; + iframe.srcdoc = ` +