ladybird/Libraries/LibWeb/Encoding/TextDecoderStream.cpp
Andreas Kling b6bef6b688 Libraries: Use UTF-16 for JS-visible runtime strings
Produce JS-visible string results as UTF-16 at their source, including
numeric formatting, BigInt and BigFraction formatting, URI encoding,
console formatting, parser errors, regular expression errors, Intl and
Temporal records, LibUnicode locale boundaries, and LibWeb bindings.

Handle fractional radix formatting through the UTF-16 builder view.
2026-06-22 19:51:25 +02:00

162 lines
7.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/DataView.h>
#include <LibJS/Runtime/PrimitiveString.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/TextDecoder.h>
#include <LibWeb/Bindings/TextDecoderStream.h>
#include <LibWeb/Encoding/TextDecoderStream.h>
#include <LibWeb/Streams/TransformStream.h>
#include <LibWeb/Streams/TransformStreamOperations.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::Encoding {
GC_DEFINE_ALLOCATOR(TextDecoderStream);
// https://encoding.spec.whatwg.org/#dom-textdecoderstream
WebIDL::ExceptionOr<GC::Ref<TextDecoderStream>> TextDecoderStream::construct_impl(JS::Realm& realm, FlyString label, Bindings::TextDecoderOptions const& options)
{
// 1. Let encoding be the result of getting an encoding from label.
auto encoding = TextCodec::get_standardized_encoding(label);
// 2. If encoding is failure or replacement, then throw a RangeError.
if (!encoding.has_value() || encoding->equals_ignoring_ascii_case("replacement"sv))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, MUST(String::formatted("Invalid encoding {}", label)) };
// 3. Set thiss encoding to encoding.
auto lowercase_encoding_name = encoding.value().to_ascii_lowercase_string();
// 4. If options["fatal"] is true, then set thiss error mode to "fatal".
auto error_mode = options.fatal ? ErrorMode::Fatal : ErrorMode::Replacement;
// 5. Set thiss ignore BOM to options["ignoreBOM"].
auto ignore_bom = options.ignore_bom;
// 6. Set thiss decoder to a new instance of thiss encodings decoder, and set thiss I/O queue to a new I/O queue.
auto decoder = TextCodec::decoder_for_exact_name(encoding.value());
VERIFY(decoder.has_value());
// NB: Steps 7-11 — we create the TransformStream and the TextDecoderStream first so that we can refer to the
// stream from the transform/flush algorithms.
// 9. Let transformStream be a new TransformStream.
auto transform_stream = realm.create<Streams::TransformStream>(realm);
auto stream = realm.create<TextDecoderStream>(realm, transform_stream, *decoder, lowercase_encoding_name, error_mode, ignore_bom);
// 7. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the decode and enqueue a chunk
// algorithm with this and chunk.
auto transform_algorithm = GC::create_function(realm.heap(), [stream](JS::Value chunk) -> GC::Ref<WebIDL::Promise> {
auto& realm = stream->realm();
if (auto result = stream->decode_and_enqueue_chunk(chunk); result.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, result.release_error());
return WebIDL::create_resolved_promise(realm, JS::js_undefined());
});
// 8. Let flushAlgorithm be an algorithm which takes no arguments and runs the flush and enqueue algorithm with this.
auto flush_algorithm = GC::create_function(realm.heap(), [stream]() -> GC::Ref<WebIDL::Promise> {
auto& realm = stream->realm();
if (auto result = stream->flush_and_enqueue(); result.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, result.release_error());
return WebIDL::create_resolved_promise(realm, JS::js_undefined());
});
// 10. Set up transformStream with transformAlgorithm set to transformAlgorithm and flushAlgorithm set to flushAlgorithm.
transform_stream->set_up(transform_algorithm, flush_algorithm);
// 11. Set thiss transform to transformStream.
// NB: Done via the GenericTransformStreamMixin constructor above.
return stream;
}
TextDecoderStream::TextDecoderStream(JS::Realm& realm, GC::Ref<Streams::TransformStream> transform, TextCodec::Decoder& decoder, FlyString encoding, ErrorMode error_mode, bool ignore_bom)
: Bindings::PlatformObject(realm)
, Streams::GenericTransformStreamMixin(transform)
, TextDecoderCommonMixin(decoder, move(encoding), error_mode, ignore_bom)
, m_streaming_decoder(make<TextCodec::StreamingDecoder>(m_encoding))
{
}
TextDecoderStream::~TextDecoderStream() = default;
void TextDecoderStream::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(TextDecoderStream);
Base::initialize(realm);
}
void TextDecoderStream::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
Streams::GenericTransformStreamMixin::visit_edges(visitor);
}
// https://encoding.spec.whatwg.org/#decode-and-enqueue-a-chunk
WebIDL::ExceptionOr<void> TextDecoderStream::decode_and_enqueue_chunk(JS::Value chunk)
{
auto& realm = this->realm();
auto& vm = realm.vm();
// 1. Let bufferSource be the result of converting chunk to an AllowSharedBufferSource.
if (!WebIDL::is_buffer_source_type(chunk))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Chunk is not a BufferSource"sv };
// 2. Push a copy of bufferSource to decoder's I/O queue.
auto buffer_or_error = WebIDL::get_buffer_source_copy(chunk.as_object());
if (buffer_or_error.is_error())
return WebIDL::OperationError::create(realm, "Failed to copy bytes from BufferSource"_utf16);
auto buffer = buffer_or_error.release_value();
auto decoded = TRY_OR_THROW_OOM(vm, m_streaming_decoder->to_utf8(buffer.bytes()));
// 3-4. Run "processing an item" until the input is exhausted, accumulating the output, then enqueue any non-empty
// result. If processing returns error, throw a TypeError.
return enqueue_decoded_output(decoded);
}
// https://encoding.spec.whatwg.org/#flush-and-enqueue
WebIDL::ExceptionOr<void> TextDecoderStream::flush_and_enqueue()
{
// 1-3. Drain decoder's I/O queue and run "processing an item" to completion.
auto decoded = TRY_OR_THROW_OOM(vm(), m_streaming_decoder->finish());
return enqueue_decoded_output(decoded);
}
WebIDL::ExceptionOr<void> TextDecoderStream::enqueue_decoded_output(String const& decoded)
{
auto& realm = this->realm();
auto& vm = realm.vm();
// https://encoding.spec.whatwg.org/#concept-td-serialize
// FIXME: The underlying TextCodec decoders currently strip leading BOMs unconditionally for UTF-8 and UTF-16BE/LE,
// so the "ignore BOM" flag is effectively ignored here. Once the decoders accept a "preserve BOM" mode,
// plumb m_ignore_bom through and strip the BOM from `decoded` only when m_ignore_bom is false.
if (!m_bom_seen && !decoded.is_empty())
m_bom_seen = true;
if (decoded.is_empty())
return {};
// If decoder's error mode is "fatal" and processing produced any error, throw a TypeError.
// NB: We can only detect this approximately by looking for U+FFFD in the decoded output, which the underlying
// decoder substitutes for invalid sequences. This matches the existing TextDecoder.decode() behavior.
if (fatal() && decoded.contains(0xFFFD))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Decoding failed"sv };
auto js_string = JS::PrimitiveString::create(vm, Utf16String::from_utf8(decoded));
return Streams::transform_stream_default_controller_enqueue(*m_transform->controller(), js_string);
}
}