// Copyright (c) 2021, 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. import 'dart:async'; import 'dart:typed_data'; import 'byte_collector.dart' show collectBytes; /// Utility class for reading elements from a _chunked stream_. /// /// A _chunked stream_ is a stream where each event is a chunk of elements. /// Byte-streams with the type `Stream>` is common of example of this. /// As illustrated in the example below, this utility class makes it easy to /// read a _chunked stream_ using custom chunk sizes and sub-stream sizes, /// without managing partially read chunks. /// /// ```dart /// final r = ChunkedStreamReader(File('myfile.txt').openRead()); /// try { /// // Read the first 4 bytes /// final firstBytes = await r.readChunk(4); /// if (firstBytes.length < 4) { /// throw Exception('myfile.txt has less than 4 bytes'); /// } /// /// // Read next 8 kilobytes as a substream /// Stream> substream = r.readStream(8 * 1024); /// /// ... /// } finally { /// // We always cancel the ChunkedStreamReader, this ensures the underlying /// // stream is cancelled. /// r.cancel(); /// } /// ``` /// /// The read-operations [readChunk] and [readStream] must not be invoked until /// the future from a previous call has completed. class ChunkedStreamReader { /// Iterator over underlying stream. /// /// The reader requests data from this input whenever requests on the /// reader cannot be fulfilled with the already fetched data. final StreamIterator> _input; /// Sentinel value used for [_buffer] when we have no value. final List _emptyList = const []; /// Last partially consumed chunk received from [_input]. /// /// Elements up to [_offset] have already been consumed and should not be /// consumed again. List _buffer = []; /// Offset into [_buffer] after data which have already been emitted. /// /// The offset is between `0` and `_buffer.length`, both inclusive. /// The data in [_buffer] from [_offset] and forward have not yet been /// emitted by the chunked stream reader, the data before [_offset] has. int _offset = 0; /// Whether a read request is currently being processed. /// /// Is `true` while a request is in progress. /// While a read request, like [readChunk] or [readStream], is being /// processed, no new requests can be made. /// New read attempts will throw instead. bool _reading = false; factory ChunkedStreamReader(Stream> stream) => ChunkedStreamReader._(StreamIterator(stream)); ChunkedStreamReader._(this._input); /// Read next [size] elements from _chunked stream_, buffering to create a /// chunk with [size] elements. /// /// This will read _chunks_ from the underlying _chunked stream_ until [size] /// elements have been buffered, or end-of-stream, then it returns the first /// [size] buffered elements. /// /// If end-of-stream is encountered before [size] elements is read, this /// returns a list with fewer than [size] elements (indicating end-of-stream). /// /// If the underlying stream throws, the stream is cancelled, the exception is /// propogated and further read operations will fail. /// /// Throws, if another read operation is on-going. Future> readChunk(int size) async { final result = []; await for (final chunk in readStream(size)) { result.addAll(chunk); } return result; } /// Read next [size] elements from _chunked stream_ as a sub-stream. /// /// This will pass-through _chunks_ from the underlying _chunked stream_ until /// [size] elements have been returned, or end-of-stream has been encountered. /// /// If end-of-stream is encountered before [size] elements is read, this /// returns a list with fewer than [size] elements (indicating end-of-stream). /// /// If the underlying stream throws, the stream is cancelled, the exception is /// propogated and further read operations will fail. /// /// If the sub-stream returned from [readStream] is cancelled the remaining /// unread elements up-to [size] are drained, allowing subsequent /// read-operations to proceed after cancellation. /// /// Throws, if another read-operation is on-going. Stream> readStream(int size) { RangeError.checkNotNegative(size, 'size'); if (_reading) { throw StateError('Concurrent read operations are not allowed!'); } _reading = true; Stream> substream() async* { // While we have data to read while (size > 0) { // Read something into the buffer, if buffer has been consumed. assert(_offset <= _buffer.length); if (_offset == _buffer.length) { if (!(await _input.moveNext())) { // Don't attempt to read more data, as there is no more data. size = 0; _reading = false; break; } _buffer = _input.current; _offset = 0; } final remainingBuffer = _buffer.length - _offset; if (remainingBuffer > 0) { if (remainingBuffer >= size) { List output; if (_buffer is Uint8List) { output = Uint8List.sublistView( _buffer as Uint8List, _offset, _offset + size) as List; } else { output = _buffer.sublist(_offset, _offset + size); } _offset += size; size = 0; yield output; _reading = false; break; } final output = _offset == 0 ? _buffer : _buffer.sublist(_offset); size -= remainingBuffer; _buffer = _emptyList; _offset = 0; yield output; } } } final c = StreamController>(); c.onListen = () => c.addStream(substream()).whenComplete(c.close); c.onCancel = () async { while (size > 0) { assert(_offset <= _buffer.length); if (_buffer.length == _offset) { if (!await _input.moveNext()) { size = 0; // no more data break; } _buffer = _input.current; _offset = 0; } final remainingBuffer = _buffer.length - _offset; if (remainingBuffer >= size) { _offset += size; size = 0; break; } size -= remainingBuffer; _buffer = _emptyList; _offset = 0; } _reading = false; }; return c.stream; } /// Cancel the underlying _chunked stream_. /// /// If a future from [readChunk] or [readStream] is still pending then /// [cancel] behaves as if the underlying stream ended early. That is a future /// from [readChunk] may return a partial chunk smaller than the request size. /// /// It is always safe to call [cancel], even if the underlying stream was read /// to completion. /// /// It can be a good idea to call [cancel] in a `finally`-block when done /// using the [ChunkedStreamReader], this mitigates risk of leaking resources. Future cancel() async => await _input.cancel(); } /// Extensions for using [ChunkedStreamReader] with byte-streams. extension ChunkedStreamReaderByteStreamExt on ChunkedStreamReader { /// Read bytes into a [Uint8List]. /// /// This does the same as [readChunk], except it uses [collectBytes] to create /// a [Uint8List], which offers better performance. Future readBytes(int size) async => await collectBytes(readStream(size)); }