// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of "dart:convert"; /// A sink for converters to efficiently transmit String data. /// /// Instead of limiting the interface to one non-chunked [String] it accepts /// partial strings or can be transformed into a byte sink that /// accepts UTF-8 code units. /// /// The [StringConversionSink] class provides a default implementation of /// [add], [asUtf8Sink] and [asStringSink]. abstract mixin class StringConversionSink implements ChunkedConversionSink { const StringConversionSink(); factory StringConversionSink.withCallback(void callback(String accumulated)) = _StringCallbackSink; factory StringConversionSink.from(Sink sink) = _StringAdapterSink; /// Creates a new instance wrapping the given [sink]. /// /// Every string that is added to the returned instance is forwarded to /// the [sink]. The instance is allowed to buffer and is not required to /// forward immediately. factory StringConversionSink.fromStringSink(StringSink sink) = _StringSinkConversionSink; /// Adds the next [chunk] to `this`. /// /// Adds the substring defined by [start] and [end]-exclusive to `this`. /// /// If [isLast] is `true` closes `this`. void addSlice(String chunk, int start, int end, bool isLast); void add(String str) { addSlice(str, 0, str.length, false); } /// Returns `this` as a sink that accepts UTF-8 input. /// /// If used, this method must be the first and only call to `this`. It /// invalidates `this`. All further operations must be performed on the result. ByteConversionSink asUtf8Sink(bool allowMalformed) { return _Utf8ConversionSink(this, allowMalformed); } /// Returns `this` as a [ClosableStringSink]. /// /// If used, this method must be the first and only call to `this`. It /// invalidates `this`. All further operations must be performed on the result. ClosableStringSink asStringSink() { return _StringConversionSinkAsStringSinkAdapter(this); } } /// A [ClosableStringSink] extends the [StringSink] interface by adding a /// `close` method. abstract interface class ClosableStringSink implements StringSink { /// Creates a new instance combining a [StringSink] [sink] and a callback /// [onClose] which is invoked when the returned instance is closed. factory ClosableStringSink.fromStringSink(StringSink sink, void onClose()) = _ClosableStringSink; /// Closes `this` and flushes any outstanding data. void close(); } /// This class wraps an existing [StringSink] and invokes a /// closure when [close] is invoked. class _ClosableStringSink implements ClosableStringSink { final void Function() _callback; final StringSink _sink; _ClosableStringSink(this._sink, this._callback); void close() { _callback(); } void writeCharCode(int charCode) { _sink.writeCharCode(charCode); } void write(Object? o) { _sink.write(o); } void writeln([Object? o = ""]) { _sink.writeln(o); } void writeAll(Iterable objects, [String separator = ""]) { _sink.writeAll(objects, separator); } } /// This class wraps an existing [StringConversionSink] and exposes a /// [ClosableStringSink] interface. The wrapped sink only needs to implement /// `add` and `close`. // TODO(floitsch): make this class public? class _StringConversionSinkAsStringSinkAdapter implements ClosableStringSink { static const _MIN_STRING_SIZE = 16; final StringBuffer _buffer; final StringConversionSink _chunkedSink; _StringConversionSinkAsStringSinkAdapter(this._chunkedSink) : _buffer = StringBuffer(); void close() { if (_buffer.isNotEmpty) _flush(); _chunkedSink.close(); } void writeCharCode(int charCode) { _buffer.writeCharCode(charCode); if (_buffer.length > _MIN_STRING_SIZE) _flush(); } void write(Object? o) { if (_buffer.isNotEmpty) _flush(); _chunkedSink.add(o.toString()); } void writeln([Object? o = ""]) { _buffer.writeln(o); if (_buffer.length > _MIN_STRING_SIZE) _flush(); } void writeAll(Iterable objects, [String separator = ""]) { if (_buffer.isNotEmpty) _flush(); var iterator = objects.iterator; if (!iterator.moveNext()) return; if (separator.isEmpty) { do { _chunkedSink.add(iterator.current.toString()); } while (iterator.moveNext()); } else { _chunkedSink.add(iterator.current.toString()); while (iterator.moveNext()) { write(separator); _chunkedSink.add(iterator.current.toString()); } } } void _flush() { var accumulated = _buffer.toString(); _buffer.clear(); _chunkedSink.add(accumulated); } } /// This class provides a base-class for converters that need to accept String /// inputs. typedef StringConversionSinkBase = StringConversionSink; /// This class provides a mixin for converters that need to accept String /// inputs. typedef StringConversionSinkMixin = StringConversionSink; /// This class is a [StringConversionSink] that wraps a [StringSink]. class _StringSinkConversionSink extends StringConversionSink { final TStringSink _stringSink; _StringSinkConversionSink(this._stringSink); void close() {} void addSlice(String str, int start, int end, bool isLast) { if (start != 0 || end != str.length) { for (var i = start; i < end; i++) { _stringSink.writeCharCode(str.codeUnitAt(i)); } } else { _stringSink.write(str); } if (isLast) close(); } void add(String str) { _stringSink.write(str); } ByteConversionSink asUtf8Sink(bool allowMalformed) { return _Utf8StringSinkAdapter(this, _stringSink, allowMalformed); } ClosableStringSink asStringSink() { return ClosableStringSink.fromStringSink(_stringSink, close); } } /// This class accumulates all chunks into one string /// and invokes a callback when the sink is closed. /// /// This class can be used to terminate a chunked conversion. class _StringCallbackSink extends _StringSinkConversionSink { final void Function(String) _callback; _StringCallbackSink(this._callback) : super(StringBuffer()); void close() { var accumulated = _stringSink.toString(); _stringSink.clear(); _callback(accumulated); } ByteConversionSink asUtf8Sink(bool allowMalformed) { return _Utf8StringSinkAdapter(this, _stringSink, allowMalformed); } } /// This class adapts a simple [ChunkedConversionSink] to a /// [StringConversionSink]. /// /// All additional methods of the [StringConversionSink] (compared to the /// ChunkedConversionSink) are redirected to the `add` method. class _StringAdapterSink extends StringConversionSink { final Sink _sink; _StringAdapterSink(this._sink); void add(String str) { _sink.add(str); } void addSlice(String str, int start, int end, bool isLast) { if (start == 0 && end == str.length) { add(str); } else { add(str.substring(start, end)); } if (isLast) close(); } void close() { _sink.close(); } } /// Decodes UTF-8 code units and stores them in a [StringSink]. /// /// The `Sink` provided is closed when this sink is closed. class _Utf8StringSinkAdapter extends ByteConversionSink { final _Utf8Decoder _decoder; final Sink _sink; final StringSink _stringSink; _Utf8StringSinkAdapter(this._sink, this._stringSink, bool allowMalformed) : _decoder = _Utf8Decoder(allowMalformed); void close() { _decoder.flush(_stringSink); _sink.close(); } void add(List chunk) { addSlice(chunk, 0, chunk.length, false); } void addSlice( List codeUnits, int startIndex, int endIndex, bool isLast, ) { _stringSink.write(_decoder.convertChunked(codeUnits, startIndex, endIndex)); if (isLast) close(); } } /// Decodes UTF-8 code units. /// /// Forwards the decoded strings to the given [StringConversionSink]. // TODO(floitsch): make this class public? class _Utf8ConversionSink extends ByteConversionSink { final _Utf8Decoder _decoder; final StringConversionSink _chunkedSink; final StringBuffer _buffer; _Utf8ConversionSink(StringConversionSink sink, bool allowMalformed) : this._(sink, StringBuffer(), allowMalformed); _Utf8ConversionSink._( this._chunkedSink, StringBuffer stringBuffer, bool allowMalformed, ) : _decoder = _Utf8Decoder(allowMalformed), _buffer = stringBuffer; void close() { _decoder.flush(_buffer); if (_buffer.isNotEmpty) { var accumulated = _buffer.toString(); _buffer.clear(); _chunkedSink.addSlice(accumulated, 0, accumulated.length, true); } else { _chunkedSink.close(); } } void add(List chunk) { addSlice(chunk, 0, chunk.length, false); } void addSlice(List chunk, int startIndex, int endIndex, bool isLast) { _buffer.write(_decoder.convertChunked(chunk, startIndex, endIndex)); if (_buffer.isNotEmpty) { var accumulated = _buffer.toString(); _chunkedSink.addSlice(accumulated, 0, accumulated.length, isLast); _buffer.clear(); return; } if (isLast) close(); } }