File: src\runtime\src\libraries\System.Private.CoreLib\src\System\IO\StreamReader.cs
Web Access
Project: src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace System.IO
{
    // This class implements a TextReader for reading characters to a Stream.
    // This is designed for character input in a particular Encoding,
    // whereas the Stream class is designed for byte input and output.
    public class StreamReader : TextReader
    {
        // StreamReader.Null is threadsafe.
        public static new readonly StreamReader Null = new NullStreamReader();

        // Using a 1K byte buffer and a 4K FileStream buffer works out pretty well
        // perf-wise.  On even a 40 MB text file, any perf loss by using a 4K
        // buffer is negated by the win of allocating a smaller byte[], which
        // saves construction time.  This does break adaptive buffering,
        // but this is slightly faster.
        private const int DefaultBufferSize = 1024;  // Byte buffer size
        private const int MinBufferSize = 128;

        private readonly Stream _stream;
        private Encoding _encoding = null!; // only null in NullStreamReader where this is never used
        private Decoder _decoder = null!; // only null in NullStreamReader where this is never used
        private readonly byte[] _byteBuffer = null!; // only null in NullStreamReader where this is never used
        private char[] _charBuffer = null!; // only null in NullStreamReader where this is never used
        private int _charPos;
        private int _charLen;
        // Record the number of valid bytes in the byteBuffer, for a few checks.
        private int _byteLen;
        // This is used only for preamble detection
        private int _bytePos;

        // This is the maximum number of chars we can get from one call to
        // ReadBuffer.  Used so ReadBuffer can tell when to copy data into
        // a user's char[] directly, instead of our internal char[].
        private int _maxCharsPerBuffer;

        /// <summary>True if the writer has been disposed; otherwise, false.</summary>
        private bool _disposed;

        // We will support looking for byte order marks in the stream and trying
        // to decide what the encoding might be from the byte order marks, IF they
        // exist.  But that's all we'll do.
        private bool _detectEncoding;

        // Whether we must still check for the encoding's given preamble at the
        // beginning of this file.
        private bool _checkPreamble;

        // Whether the stream is most likely not going to give us back as much
        // data as we want the next time we call it.  We must do the computation
        // before we do any byte order mark handling and save the result.  Note
        // that we need this to allow users to handle streams used for an
        // interactive protocol, where they block waiting for the remote end
        // to send a response, like logging in on a Unix machine.
        private bool _isBlocked;

        // The intent of this field is to leave open the underlying stream when
        // disposing of this StreamReader.  A name like _leaveOpen is better,
        // but this type is serializable, and this field's name was _closable.
        private readonly bool _closable;  // Whether to close the underlying stream.

        // We don't guarantee thread safety on StreamReader, but we should at
        // least prevent users from trying to read anything while an Async
        // read from the same thread is in progress. We track this with the
        // following fields.
        //
        // Generally we prefer to use the bool _asyncIOInProgress, but in
        // certain cases for async1 this would require introducing a wrapper
        // state machine, and in those cases we use the Task _asyncReadTask
        // instead.
        //
        private bool _asyncIOInProgress;
        private Task _asyncReadTask = Task.CompletedTask;

        private void CheckAsyncTaskInProgress()
        {
            // We are not locking this access because this is not meant to guarantee thread safety.
            // We are simply trying to deter calling any Read APIs while an async Read from the same thread is in progress.
            if (_asyncIOInProgress || !_asyncReadTask.IsCompleted)
            {
                ThrowAsyncIOInProgress();
            }
        }

        [DoesNotReturn]
        private static void ThrowAsyncIOInProgress() =>
            throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress);

        private ThrowOnReadsScope GuardAgainstOtherReads()
        {
            return new ThrowOnReadsScope(this);
        }

        private readonly struct ThrowOnReadsScope : IDisposable
        {
            private readonly StreamReader _reader;

            public ThrowOnReadsScope(StreamReader reader)
            {
                reader._asyncIOInProgress = true;
                _reader = reader;
            }

            public void Dispose() => _reader._asyncIOInProgress = false;
        }

        // StreamReader by default will ignore illegal UTF8 characters. We don't want to
        // throw here because we want to be able to read ill-formed data without choking.
        // The high level goal is to be tolerant of encoding errors when we read and very strict
        // when we write. Hence, default StreamWriter encoding will throw on error.

        private StreamReader()
        {
            Debug.Assert(this is NullStreamReader);
            _stream = Stream.Null;
            _closable = true;
        }

        public StreamReader(Stream stream)
            : this(stream, detectEncodingFromByteOrderMarks: true)
        {
        }

        public StreamReader(Stream stream, bool detectEncodingFromByteOrderMarks)
            : this(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks, DefaultBufferSize, leaveOpen: false)
        {
        }

        public StreamReader(Stream stream, Encoding? encoding)
            : this(stream, encoding, detectEncodingFromByteOrderMarks: true, DefaultBufferSize, leaveOpen: false)
        {
        }

        public StreamReader(Stream stream, Encoding? encoding, bool detectEncodingFromByteOrderMarks)
            : this(stream, encoding, detectEncodingFromByteOrderMarks, DefaultBufferSize, leaveOpen: false)
        {
        }

        // Creates a new StreamReader for the given stream.  The
        // character encoding is set by encoding and the buffer size,
        // in bytes, is set by bufferSize.
        //
        // Note that detectEncodingFromByteOrderMarks is a very
        // loose attempt at detecting the encoding by looking at the first
        // 3 bytes of the stream.  It will recognize UTF-8, little endian
        // unicode, and big endian unicode text, but that's it.  If neither
        // of those three match, it will use the Encoding you provided.
        //
        public StreamReader(Stream stream, Encoding? encoding, bool detectEncodingFromByteOrderMarks, int bufferSize)
            : this(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, leaveOpen: false)
        {
        }

        public StreamReader(Stream stream, Encoding? encoding = null, bool detectEncodingFromByteOrderMarks = true, int bufferSize = -1, bool leaveOpen = false)
        {
            if (stream == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stream);
            }

            if (!stream.CanRead)
            {
                throw new ArgumentException(SR.Argument_StreamNotReadable);
            }

            if (bufferSize == -1)
            {
                bufferSize = DefaultBufferSize;
            }
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);

            _stream = stream;
            _encoding = encoding ??= Encoding.UTF8;
            _decoder = encoding.GetDecoder();
            if (bufferSize < MinBufferSize)
            {
                bufferSize = MinBufferSize;
            }

            _byteBuffer = new byte[bufferSize];
            _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize);
            _charBuffer = new char[_maxCharsPerBuffer];
            _detectEncoding = detectEncodingFromByteOrderMarks;

            // If the preamble length is larger than the byte buffer length,
            // we'll never match it and will enter an infinite loop. This
            // should never happen in practice, but just in case, we'll skip
            // the preamble check for absurdly long preambles.
            int preambleLength = encoding.Preamble.Length;
            _checkPreamble = preambleLength > 0 && preambleLength <= bufferSize;

            _closable = !leaveOpen;
        }

        public StreamReader(string path)
            : this(path, detectEncodingFromByteOrderMarks: true)
        {
        }

        public StreamReader(string path, bool detectEncodingFromByteOrderMarks)
            : this(path, Encoding.UTF8, detectEncodingFromByteOrderMarks, DefaultBufferSize)
        {
        }

        public StreamReader(string path, Encoding? encoding)
            : this(path, encoding, detectEncodingFromByteOrderMarks: true, DefaultBufferSize)
        {
        }

        public StreamReader(string path, Encoding? encoding, bool detectEncodingFromByteOrderMarks)
            : this(path, encoding, detectEncodingFromByteOrderMarks, DefaultBufferSize)
        {
        }

        public StreamReader(string path, Encoding? encoding, bool detectEncodingFromByteOrderMarks, int bufferSize)
            : this(ValidateArgsAndOpenPath(path, bufferSize), encoding, detectEncodingFromByteOrderMarks, bufferSize, leaveOpen: false)
        {
        }

        public StreamReader(string path, FileStreamOptions options)
            : this(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, options)
        {
        }

        public StreamReader(string path, Encoding? encoding, bool detectEncodingFromByteOrderMarks, FileStreamOptions options)
            : this(ValidateArgsAndOpenPath(path, options), encoding, detectEncodingFromByteOrderMarks, DefaultBufferSize)
        {
        }

        private static FileStream ValidateArgsAndOpenPath(string path, FileStreamOptions options)
        {
            ArgumentException.ThrowIfNullOrEmpty(path);
            ArgumentNullException.ThrowIfNull(options);
            if ((options.Access & FileAccess.Read) == 0)
            {
                throw new ArgumentException(SR.Argument_StreamNotReadable, nameof(options));
            }

            return new FileStream(path, options);
        }

        private static FileStream ValidateArgsAndOpenPath(string path, int bufferSize)
        {
            ArgumentException.ThrowIfNullOrEmpty(path);

            if (bufferSize != -1)
            {
                ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
            }

            return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, FileStream.DefaultBufferSize);
        }

        public override void Close()
        {
            Dispose(true);
        }

        protected override void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            _disposed = true;

            // Dispose of our resources if this StreamReader is closable.
            if (_closable)
            {
                try
                {
                    // Note that Stream.Close() can potentially throw here. So we need to
                    // ensure cleaning up internal resources, inside the finally block.
                    if (disposing)
                    {
                        _stream.Close();
                    }
                }
                finally
                {
                    _charPos = 0;
                    _charLen = 0;
                    base.Dispose(disposing);
                }
            }
        }

        public virtual Encoding CurrentEncoding => _encoding;

        public virtual Stream BaseStream => _stream;

        // DiscardBufferedData tells StreamReader to throw away its internal
        // buffer contents.  This is useful if the user needs to seek on the
        // underlying stream to a known location then wants the StreamReader
        // to start reading from this new point.  This method should be called
        // very sparingly, if ever, since it can lead to very poor performance.
        // However, it may be the only way of handling some scenarios where
        // users need to re-read the contents of a StreamReader a second time.
        public void DiscardBufferedData()
        {
            CheckAsyncTaskInProgress();

            _byteLen = 0;
            _charLen = 0;
            _charPos = 0;
            // in general we'd like to have an invariant that encoding isn't null. However,
            // for startup improvements for NullStreamReader, we want to delay load encoding.
            if (_encoding != null)
            {
                _decoder = _encoding.GetDecoder();
            }
            _isBlocked = false;
        }

        public bool EndOfStream
        {
            get
            {
                ThrowIfDisposed();
                CheckAsyncTaskInProgress();

                if (_charPos < _charLen)
                {
                    return false;
                }

                // This may block on pipes!
                int numRead = ReadBuffer();
                return numRead == 0;
            }
        }

        public override int Peek()
        {
            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (_charPos == _charLen)
            {
                if (ReadBuffer() == 0)
                {
                    return -1;
                }
            }
            return _charBuffer[_charPos];
        }

        public override int Read()
        {
            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (_charPos == _charLen)
            {
                if (ReadBuffer() == 0)
                {
                    return -1;
                }
            }
            int result = _charBuffer[_charPos];
            _charPos++;
            return result;
        }

        public override int Read(char[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);

            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (buffer.Length - index < count)
            {
                throw new ArgumentException(SR.Argument_InvalidOffLen);
            }

            return ReadSpan(new Span<char>(buffer, index, count));
        }

        public override int Read(Span<char> buffer) =>
            GetType() == typeof(StreamReader) ? ReadSpan(buffer) :
            base.Read(buffer); // Defer to Read(char[], ...) if a derived type may have previously overridden it

        private int ReadSpan(Span<char> buffer)
        {
            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            int charsRead = 0;
            // As a perf optimization, if we had exactly one buffer's worth of
            // data read in, let's try writing directly to the user's buffer.
            bool readToUserBuffer = false;
            int count = buffer.Length;
            while (count > 0)
            {
                int n = _charLen - _charPos;
                if (n == 0)
                {
                    n = ReadBuffer(buffer.Slice(charsRead), out readToUserBuffer);
                }
                if (n == 0)
                {
                    break;  // We're at EOF
                }
                if (n > count)
                {
                    n = count;
                }
                if (!readToUserBuffer)
                {
                    new Span<char>(_charBuffer, _charPos, n).CopyTo(buffer.Slice(charsRead));
                    _charPos += n;
                }

                charsRead += n;
                count -= n;
                // This function shouldn't block for an indefinite amount of time,
                // or reading from a network stream won't work right.  If we got
                // fewer bytes than we requested, then we want to break right here.
                if (_isBlocked)
                {
                    break;
                }
            }

            return charsRead;
        }

        public override string ReadToEnd()
        {
            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            // Call ReadBuffer, then pull data out of charBuffer.
            StringBuilder sb = new StringBuilder(_charLen - _charPos);
            do
            {
                sb.Append(_charBuffer, _charPos, _charLen - _charPos);
                _charPos = _charLen;  // Note we consumed these characters
                ReadBuffer();
            } while (_charLen > 0);
            return sb.ToString();
        }

        public override int ReadBlock(char[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);

            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (buffer.Length - index < count)
            {
                throw new ArgumentException(SR.Argument_InvalidOffLen);
            }
            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            return base.ReadBlock(buffer, index, count);
        }

        public override int ReadBlock(Span<char> buffer)
        {
            if (GetType() != typeof(StreamReader))
            {
                // Defer to Read(char[], ...) if a derived type may have previously overridden it.
                return base.ReadBlock(buffer);
            }

            int i, n = 0;
            do
            {
                i = ReadSpan(buffer.Slice(n));
                n += i;
            } while (i > 0 && n < buffer.Length);
            return n;
        }

        // Trims n bytes from the front of the buffer.
        private void CompressBuffer(int n)
        {
            Debug.Assert(_byteLen >= n, "CompressBuffer was called with a number of bytes greater than the current buffer length.  Are two threads using this StreamReader at the same time?");
            byte[] byteBuffer = _byteBuffer;
            _ = byteBuffer.Length; // allow JIT to prove object is not null
            new ReadOnlySpan<byte>(byteBuffer, n, _byteLen - n).CopyTo(byteBuffer);
            _byteLen -= n;
        }

        private void DetectEncoding()
        {
            Debug.Assert(_byteLen >= 2, "Caller should've validated that at least 2 bytes were available.");

            byte[] byteBuffer = _byteBuffer;
            _detectEncoding = false;
            bool changedEncoding = false;

            ushort firstTwoBytes = BinaryPrimitives.ReadUInt16LittleEndian(byteBuffer);
            if (firstTwoBytes == 0xFFFE)
            {
                // Big Endian Unicode
                _encoding = Encoding.BigEndianUnicode;
                CompressBuffer(2);
                changedEncoding = true;
            }
            else if (firstTwoBytes == 0xFEFF)
            {
                // Little Endian Unicode, or possibly little endian UTF32
                if (_byteLen < 4 || byteBuffer[2] != 0 || byteBuffer[3] != 0)
                {
                    _encoding = Encoding.Unicode;
                    CompressBuffer(2);
                    changedEncoding = true;
                }
                else
                {
                    _encoding = Encoding.UTF32;
                    CompressBuffer(4);
                    changedEncoding = true;
                }
            }
            else if (_byteLen >= 3 && firstTwoBytes == 0xBBEF && byteBuffer[2] == 0xBF)
            {
                // UTF-8
                _encoding = Encoding.UTF8;
                CompressBuffer(3);
                changedEncoding = true;
            }
            else if (_byteLen >= 4 && firstTwoBytes == 0 && byteBuffer[2] == 0xFE && byteBuffer[3] == 0xFF)
            {
                // Big Endian UTF32
                _encoding = new UTF32Encoding(bigEndian: true, byteOrderMark: true);
                CompressBuffer(4);
                changedEncoding = true;
            }
            else if (_byteLen == 2)
            {
                _detectEncoding = true;
            }
            // Note: in the future, if we change this algorithm significantly,
            // we can support checking for the preamble of the given encoding.

            if (changedEncoding)
            {
                _decoder = _encoding.GetDecoder();
                int newMaxCharsPerBuffer = _encoding.GetMaxCharCount(byteBuffer.Length);
                if (newMaxCharsPerBuffer > _maxCharsPerBuffer)
                {
                    _charBuffer = new char[newMaxCharsPerBuffer];
                }
                _maxCharsPerBuffer = newMaxCharsPerBuffer;
            }
        }

        // Trims the preamble bytes from the byteBuffer. This routine can be called multiple times
        // and we will buffer the bytes read until the preamble is matched or we determine that
        // there is no match. If there is no match, every byte read previously will be available
        // for further consumption. If there is a match, we will compress the buffer for the
        // leading preamble bytes
        private bool IsPreamble()
        {
            if (!_checkPreamble)
            {
                return false;
            }

            return IsPreambleWorker(); // move this call out of the hot path
            bool IsPreambleWorker()
            {
                Debug.Assert(_checkPreamble);
                ReadOnlySpan<byte> preamble = _encoding.Preamble;

                Debug.Assert(_bytePos < preamble.Length, "_compressPreamble was called with the current bytePos greater than the preamble buffer length.  Are two threads using this StreamReader at the same time?");
                int len = Math.Min(_byteLen, preamble.Length);

                for (int i = _bytePos; i < len; i++)
                {
                    if (_byteBuffer[i] != preamble[i])
                    {
                        _bytePos = 0; // preamble match failed; back up to beginning of buffer
                        _checkPreamble = false;
                        return false;
                    }
                }
                _bytePos = len; // we've matched all bytes up to this point

                Debug.Assert(_bytePos <= preamble.Length, "possible bug in _compressPreamble.  Are two threads using this StreamReader at the same time?");

                if (_bytePos == preamble.Length)
                {
                    // We have a match
                    CompressBuffer(preamble.Length);
                    _bytePos = 0;
                    _checkPreamble = false;
                    _detectEncoding = false;
                }

                return _checkPreamble;
            }
        }

        internal virtual int ReadBuffer()
        {
            _charLen = 0;
            _charPos = 0;

            if (!_checkPreamble)
            {
                _byteLen = 0;
            }

            bool eofReached = false;

            do
            {
                if (_checkPreamble)
                {
                    Debug.Assert(_bytePos <= _encoding.Preamble.Length, "possible bug in _compressPreamble.  Are two threads using this StreamReader at the same time?");
                    int len = _stream.Read(_byteBuffer, _bytePos, _byteBuffer.Length - _bytePos);
                    Debug.Assert(len >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                    if (len == 0)
                    {
                        eofReached = true;
                        break;
                    }

                    _byteLen += len;
                }
                else
                {
                    Debug.Assert(_bytePos == 0, "bytePos can be non zero only when we are trying to _checkPreamble.  Are two threads using this StreamReader at the same time?");
                    _byteLen = _stream.Read(_byteBuffer, 0, _byteBuffer.Length);
                    Debug.Assert(_byteLen >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                    if (_byteLen == 0)
                    {
                        eofReached = true;
                        break;
                    }
                }

                // _isBlocked == whether we read fewer bytes than we asked for.
                // Note we must check it here because CompressBuffer or
                // DetectEncoding will change byteLen.
                _isBlocked = (_byteLen < _byteBuffer.Length);

                // Check for preamble before detect encoding. This is not to override the
                // user supplied Encoding for the one we implicitly detect. The user could
                // customize the encoding which we will loose, such as ThrowOnError on UTF8
                if (IsPreamble())
                {
                    continue;
                }

                // If we're supposed to detect the encoding and haven't done so yet,
                // do it.  Note this may need to be called more than once.
                if (_detectEncoding && _byteLen >= 2)
                {
                    DetectEncoding();
                }

                Debug.Assert(_charPos == 0 && _charLen == 0, "We shouldn't be trying to decode more data if we made progress in an earlier iteration.");
                _charLen = _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, 0, flush: false);
            } while (_charLen == 0);

            if (eofReached)
            {
                // EOF has been reached - perform final flush.
                // We need to reset _bytePos and _byteLen just in case we hadn't
                // finished processing the preamble before we reached EOF.

                Debug.Assert(_charPos == 0 && _charLen == 0, "We shouldn't be looking for EOF unless we have an empty char buffer.");
                _charLen = _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, 0, flush: true);
                _bytePos = 0;
                _byteLen = 0;
            }

            return _charLen;
        }


        // This version has a perf optimization to decode data DIRECTLY into the
        // user's buffer, bypassing StreamReader's own buffer.
        // This gives a > 20% perf improvement for our encodings across the board,
        // but only when asking for at least the number of characters that one
        // buffer's worth of bytes could produce.
        // This optimization, if run, will break SwitchEncoding, so we must not do
        // this on the first call to ReadBuffer.
        private int ReadBuffer(Span<char> userBuffer, out bool readToUserBuffer)
        {
            _charLen = 0;
            _charPos = 0;

            if (!_checkPreamble)
            {
                _byteLen = 0;
            }

            bool eofReached = false;
            int charsRead = 0;

            // As a perf optimization, we can decode characters DIRECTLY into a
            // user's char[].  We absolutely must not write more characters
            // into the user's buffer than they asked for.  Calculating
            // encoding.GetMaxCharCount(byteLen) each time is potentially very
            // expensive - instead, cache the number of chars a full buffer's
            // worth of data may produce.  Yes, this makes the perf optimization
            // less aggressive, in that all reads that asked for fewer than AND
            // returned fewer than _maxCharsPerBuffer chars won't get the user
            // buffer optimization.  This affects reads where the end of the
            // Stream comes in the middle somewhere, and when you ask for
            // fewer chars than your buffer could produce.
            readToUserBuffer = userBuffer.Length >= _maxCharsPerBuffer;

            do
            {
                Debug.Assert(charsRead == 0);

                if (_checkPreamble)
                {
                    Debug.Assert(_bytePos <= _encoding.Preamble.Length, "possible bug in _compressPreamble.  Are two threads using this StreamReader at the same time?");
                    int len = _stream.Read(_byteBuffer, _bytePos, _byteBuffer.Length - _bytePos);
                    Debug.Assert(len >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                    if (len == 0)
                    {
                        eofReached = true;
                        break;
                    }

                    _byteLen += len;
                }
                else
                {
                    Debug.Assert(_bytePos == 0, "bytePos can be non zero only when we are trying to _checkPreamble.  Are two threads using this StreamReader at the same time?");
                    _byteLen = _stream.Read(_byteBuffer, 0, _byteBuffer.Length);
                    Debug.Assert(_byteLen >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                    if (_byteLen == 0)
                    {
                        eofReached = true;
                        break;
                    }
                }

                // _isBlocked == whether we read fewer bytes than we asked for.
                // Note we must check it here because CompressBuffer or
                // DetectEncoding will change byteLen.
                _isBlocked = (_byteLen < _byteBuffer.Length);

                // Check for preamble before detect encoding. This is not to override the
                // user supplied Encoding for the one we implicitly detect. The user could
                // customize the encoding which we will loose, such as ThrowOnError on UTF8
                // Note: we don't need to recompute readToUserBuffer optimization as IsPreamble
                // doesn't change the encoding or affect _maxCharsPerBuffer
                if (IsPreamble())
                {
                    continue;
                }

                // On the first call to ReadBuffer, if we're supposed to detect the encoding, do it.
                if (_detectEncoding && _byteLen >= 2)
                {
                    DetectEncoding();
                    // DetectEncoding changes some buffer state.  Recompute this.
                    readToUserBuffer = userBuffer.Length >= _maxCharsPerBuffer;
                }

                Debug.Assert(charsRead == 0 && _charPos == 0 && _charLen == 0, "We shouldn't be trying to decode more data if we made progress in an earlier iteration.");
                if (readToUserBuffer)
                {
                    charsRead = _decoder.GetChars(new ReadOnlySpan<byte>(_byteBuffer, 0, _byteLen), userBuffer, flush: false);
                }
                else
                {
                    charsRead = _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, 0, flush: false);
                    _charLen = charsRead;  // Number of chars in StreamReader's buffer.
                }
            } while (charsRead == 0);

            if (eofReached)
            {
                // EOF has been reached - perform final flush.
                // We need to reset _bytePos and _byteLen just in case we hadn't
                // finished processing the preamble before we reached EOF.

                Debug.Assert(charsRead == 0 && _charPos == 0 && _charLen == 0, "We shouldn't be looking for EOF unless we have an empty char buffer.");

                if (readToUserBuffer)
                {
                    charsRead = _decoder.GetChars(new ReadOnlySpan<byte>(_byteBuffer, 0, _byteLen), userBuffer, flush: true);
                }
                else
                {
                    charsRead = _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, 0, flush: true);
                    _charLen = charsRead;  // Number of chars in StreamReader's buffer.
                }
                _bytePos = 0;
                _byteLen = 0;
            }

            _isBlocked &= charsRead < userBuffer.Length;

            return charsRead;
        }


        // Reads a line. A line is defined as a sequence of characters followed by
        // a carriage return ('\r'), a line feed ('\n'), or a carriage return
        // immediately followed by a line feed. The resulting string does not
        // contain the terminating carriage return and/or line feed. The returned
        // value is null if the end of the input stream has been reached.
        //
        public override unsafe string? ReadLine()
        {
            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (_charPos == _charLen)
            {
                if (ReadBuffer() == 0)
                {
                    return null;
                }
            }

            var vsb = new ValueStringBuilder(stackalloc char[256]);
            do
            {
                // Look for '\r' or \'n'.
                ReadOnlySpan<char> charBufferSpan = _charBuffer.AsSpan(_charPos, _charLen - _charPos);
                Debug.Assert(!charBufferSpan.IsEmpty, "ReadBuffer returned > 0 but didn't bump _charLen?");

                int idxOfNewline = charBufferSpan.IndexOfAny('\r', '\n');
                if (idxOfNewline >= 0)
                {
                    string retVal;
                    if (vsb.Length == 0)
                    {
                        retVal = new string(charBufferSpan.Slice(0, idxOfNewline));
                    }
                    else
                    {
                        retVal = string.Concat(vsb.AsSpan(), charBufferSpan.Slice(0, idxOfNewline));
                        vsb.Dispose();
                    }

                    char matchedChar = charBufferSpan[idxOfNewline];
                    _charPos += idxOfNewline + 1;

                    // If we found '\r', consume any immediately following '\n'.
                    if (matchedChar == '\r')
                    {
                        if (_charPos < _charLen || ReadBuffer() > 0)
                        {
                            if (_charBuffer[_charPos] == '\n')
                            {
                                _charPos++;
                            }
                        }
                    }

                    return retVal;
                }

                // We didn't find '\r' or '\n'. Add it to the StringBuilder
                // and loop until we reach a newline or EOF.

                vsb.Append(charBufferSpan);
            } while (ReadBuffer() > 0);

            return vsb.ToString();
        }

        public override Task<string?> ReadLineAsync() =>
            ReadLineAsync(default).AsTask();

        /// <summary>
        /// Reads a line of characters asynchronously from the current stream and returns the data as a string.
        /// </summary>
        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
        /// <returns>A value task that represents the asynchronous read operation. The value of the <c>TResult</c>
        /// parameter contains the next line from the stream, or is <see langword="null" /> if all of the characters have been read.</returns>
        /// <exception cref="ArgumentOutOfRangeException">The number of characters in the next line is larger than <see cref="int.MaxValue"/>.</exception>
        /// <exception cref="ObjectDisposedException">The stream reader has been disposed.</exception>
        /// <exception cref="InvalidOperationException">The reader is currently in use by a previous read operation.</exception>
        /// <example>
        /// The following example shows how to read and print all lines from the file until the end of the file is reached or the operation timed out.
        /// <code lang="C#">
        /// usingusing CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1));
CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1));
        /// using StreamReader reader = File.OpenText("existingfile.txt");
using StreamReader reader = File.OpenText("existingfile.txt");
        ///
        /// stringstring line;
line;
        /// whilewhile ((line = await reader.ReadLineAsync(tokenSource.Token)) is not null)
line = await reader.ReadLineAsync(tokenSource.Token)) is not null)
        /// {
        ///     Console.WriteLine(line);
Console.WriteLine(line);
        /// }
        /// </code>
        /// </example>
        /// <remarks>
        /// If this method is canceled via <paramref name="cancellationToken"/>, some data
        /// that has been read from the current <see cref="Stream"/> but not stored (by the
        /// <see cref="StreamReader"/>) or returned (to the caller) may be lost.
        /// </remarks>
        public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
        {
            // If we have been inherited into a subclass, the following implementation could be incorrect
            // since it does not call through to Read() which a subclass might have overridden.
            // To be safe we will only use this implementation in cases where we know it is safe to do so,
            // and delegate to our base class (which will call into Read) when we are not sure.
            if (GetType() != typeof(StreamReader))
            {
                return base.ReadLineAsync(cancellationToken);
            }

            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            return new ValueTask<string?>(ReadLineAsyncInternal(cancellationToken));
        }

        private async Task<string?> ReadLineAsyncInternal(CancellationToken cancellationToken)
        {
            using ThrowOnReadsScope _ = GuardAgainstOtherReads();

            if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0)
            {
                return null;
            }

            string retVal;
            char[]? arrayPoolBuffer = null;
            int arrayPoolBufferPos = 0;

            do
            {
                char[] charBuffer = _charBuffer;
                int charLen = _charLen;
                int charPos = _charPos;

                // Look for '\r' or \'n'.
                Debug.Assert(charPos < charLen, "ReadBuffer returned > 0 but didn't bump _charLen?");

                int idxOfNewline = charBuffer.AsSpan(charPos, charLen - charPos).IndexOfAny('\r', '\n');
                if (idxOfNewline >= 0)
                {
                    if (arrayPoolBuffer is null)
                    {
                        retVal = new string(charBuffer, charPos, idxOfNewline);
                    }
                    else
                    {
                        retVal = string.Concat(arrayPoolBuffer.AsSpan(0, arrayPoolBufferPos), charBuffer.AsSpan(charPos, idxOfNewline));
                        ArrayPool<char>.Shared.Return(arrayPoolBuffer);
                    }

                    charPos += idxOfNewline;
                    char matchedChar = charBuffer[charPos++];
                    _charPos = charPos;

                    // If we found '\r', consume any immediately following '\n'.
                    if (matchedChar == '\r')
                    {
                        if (charPos < charLen || (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) > 0)
                        {
                            if (_charBuffer[_charPos] == '\n')
                            {
                                _charPos++;
                            }
                        }
                    }

                    return retVal;
                }

                // We didn't find '\r' or '\n'. Add the read data to the pooled buffer
                // and loop until we reach a newline or EOF.
                if (arrayPoolBuffer is null)
                {
                    arrayPoolBuffer = ArrayPool<char>.Shared.Rent(charLen - charPos + 80);
                }
                else if ((arrayPoolBuffer.Length - arrayPoolBufferPos) < (charLen - charPos))
                {
                    char[] newBuffer = ArrayPool<char>.Shared.Rent(checked(arrayPoolBufferPos + charLen - charPos));
                    arrayPoolBuffer.AsSpan(0, arrayPoolBufferPos).CopyTo(newBuffer);
                    ArrayPool<char>.Shared.Return(arrayPoolBuffer);
                    arrayPoolBuffer = newBuffer;
                }
                charBuffer.AsSpan(charPos, charLen - charPos).CopyTo(arrayPoolBuffer.AsSpan(arrayPoolBufferPos));
                arrayPoolBufferPos += charLen - charPos;
            }
            while (await ReadBufferAsync(cancellationToken).ConfigureAwait(false) > 0);

            if (arrayPoolBuffer is not null)
            {
                retVal = new string(arrayPoolBuffer, 0, arrayPoolBufferPos);
                ArrayPool<char>.Shared.Return(arrayPoolBuffer);
            }
            else
            {
                retVal = string.Empty;
            }

            return retVal;
        }

        public override Task<string> ReadToEndAsync() => ReadToEndAsync(default);

        /// <summary>
        /// Reads all characters from the current position to the end of the stream asynchronously and returns them as one string.
        /// </summary>
        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
        /// <returns>A task that represents the asynchronous read operation. The value of the <c>TResult</c> parameter contains
        /// a string with the characters from the current position to the end of the stream.</returns>
        /// <exception cref="ArgumentOutOfRangeException">The number of characters is larger than <see cref="int.MaxValue"/>.</exception>
        /// <exception cref="ObjectDisposedException">The stream reader has been disposed.</exception>
        /// <exception cref="InvalidOperationException">The reader is currently in use by a previous read operation.</exception>
        /// <example>
        /// The following example shows how to read the contents of a file by using the <see cref="ReadToEndAsync(CancellationToken)"/> method.
        /// <code lang="C#">
        /// usingusing CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1));
CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1));
        /// usingusing StreamReader reader = File.OpenText("existingfile.txt");
StreamReader reader = File.OpenText("existingfile.txt");
        ///
        /// ConsoleConsole.WriteLine(await reader.ReadToEndAsync(tokenSource.Token));
WriteLine(await reader.ReadToEndAsync(tokenSource.Token));
        /// </code>
        /// </example>
        /// <remarks>
        /// If this method is canceled via <paramref name="cancellationToken"/>, some data
        /// that has been read from the current <see cref="Stream"/> but not stored (by the
        /// <see cref="StreamReader"/>) or returned (to the caller) may be lost.
        /// </remarks>
        public override Task<string> ReadToEndAsync(CancellationToken cancellationToken)
        {
            // If we have been inherited into a subclass, the following implementation could be incorrect
            // since it does not call through to Read() which a subclass might have overridden.
            // To be safe we will only use this implementation in cases where we know it is safe to do so,
            // and delegate to our base class (which will call into Read) when we are not sure.
            if (GetType() != typeof(StreamReader))
            {
                return base.ReadToEndAsync(cancellationToken);
            }

            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            return ReadToEndAsyncInternal(cancellationToken);
        }

        private async Task<string> ReadToEndAsyncInternal(CancellationToken cancellationToken)
        {
            using ThrowOnReadsScope _ = GuardAgainstOtherReads();

            // Call ReadBuffer, then pull data out of charBuffer.
            StringBuilder sb = new StringBuilder(_charLen - _charPos);
            do
            {
                int tmpCharPos = _charPos;
                sb.Append(_charBuffer, tmpCharPos, _charLen - tmpCharPos);
                _charPos = _charLen;  // We consumed these characters
                await ReadBufferAsync(cancellationToken).ConfigureAwait(false);
            } while (_charLen > 0);

            return sb.ToString();
        }

        public override Task<int> ReadAsync(char[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);

            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (buffer.Length - index < count)
            {
                throw new ArgumentException(SR.Argument_InvalidOffLen);
            }

            // If we have been inherited into a subclass, the following implementation could be incorrect
            // since it does not call through to Read() which a subclass might have overridden.
            // To be safe we will only use this implementation in cases where we know it is safe to do so,
            // and delegate to our base class (which will call into Read) when we are not sure.
            if (GetType() != typeof(StreamReader))
            {
                return base.ReadAsync(buffer, index, count);
            }

            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (RuntimeHelpers.IsRuntimeAsync())
            {
                return ReadAsyncInternalWithGuard(new Memory<char>(buffer, index, count), CancellationToken.None);
            }

            Task<int> task = ReadAsyncInternal(new Memory<char>(buffer, index, count), CancellationToken.None).AsTask();
            _asyncReadTask = task;
            return task;

            async Task<int> ReadAsyncInternalWithGuard(Memory<char> buffer, CancellationToken cancellationToken)
            {
                using ThrowOnReadsScope _ = GuardAgainstOtherReads();
                return await ReadAsyncInternal(buffer, cancellationToken).ConfigureAwait(false);
            }
        }

        public override ValueTask<int> ReadAsync(Memory<char> buffer, CancellationToken cancellationToken = default)
        {
            if (GetType() != typeof(StreamReader))
            {
                // Ensure we use existing overrides if a class already overrode existing overloads.
                return base.ReadAsync(buffer, cancellationToken);
            }

            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (cancellationToken.IsCancellationRequested)
            {
                return ValueTask.FromCanceled<int>(cancellationToken);
            }

            return ReadAsyncInternal(buffer, cancellationToken);
        }

        internal override async ValueTask<int> ReadAsyncInternal(Memory<char> buffer, CancellationToken cancellationToken)
        {
            if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0)
            {
                return 0;
            }

            int charsRead = 0;

            // As a perf optimization, if we had exactly one buffer's worth of
            // data read in, let's try writing directly to the user's buffer.
            bool readToUserBuffer = false;

            byte[] tmpByteBuffer = _byteBuffer;
            Stream tmpStream = _stream;

            int count = buffer.Length;
            while (count > 0)
            {
                // n is the characters available in _charBuffer
                int n = _charLen - _charPos;

                // charBuffer is empty, let's read from the stream
                if (n == 0)
                {
                    _charLen = 0;
                    _charPos = 0;

                    if (!_checkPreamble)
                    {
                        _byteLen = 0;
                    }

                    readToUserBuffer = count >= _maxCharsPerBuffer;

                    // We loop here so that we read in enough bytes to yield at least 1 char.
                    // We break out of the loop if the stream is blocked (EOF is reached).
                    do
                    {
                        Debug.Assert(n == 0);

                        if (_checkPreamble)
                        {
                            Debug.Assert(_bytePos <= _encoding.Preamble.Length, "possible bug in _compressPreamble.  Are two threads using this StreamReader at the same time?");
                            int tmpBytePos = _bytePos;
                            int len = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer, tmpBytePos, tmpByteBuffer.Length - tmpBytePos), cancellationToken).ConfigureAwait(false);
                            Debug.Assert(len >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                            if (len == 0)
                            {
                                // EOF but we might have buffered bytes from previous
                                // attempts to detect preamble that needs to be decoded now
                                if (_byteLen > 0)
                                {
                                    if (readToUserBuffer)
                                    {
                                        n = _decoder.GetChars(new ReadOnlySpan<byte>(tmpByteBuffer, 0, _byteLen), buffer.Span.Slice(charsRead), flush: false);
                                        _charLen = 0;  // StreamReader's buffer is empty.
                                    }
                                    else
                                    {
                                        n = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, 0);
                                        _charLen += n;  // Number of chars in StreamReader's buffer.
                                    }
                                }

                                // How can part of the preamble yield any chars?
                                Debug.Assert(n == 0);

                                _isBlocked = true;
                                break;
                            }
                            else
                            {
                                _byteLen += len;
                            }
                        }
                        else
                        {
                            Debug.Assert(_bytePos == 0, "_bytePos can be non zero only when we are trying to _checkPreamble.  Are two threads using this StreamReader at the same time?");

                            _byteLen = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer), cancellationToken).ConfigureAwait(false);

                            Debug.Assert(_byteLen >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                            if (_byteLen == 0)  // EOF
                            {
                                _isBlocked = true;
                                break;
                            }
                        }

                        // _isBlocked == whether we read fewer bytes than we asked for.
                        // Note we must check it here because CompressBuffer or
                        // DetectEncoding will change _byteLen.
                        _isBlocked = (_byteLen < tmpByteBuffer.Length);

                        // Check for preamble before detect encoding. This is not to override the
                        // user supplied Encoding for the one we implicitly detect. The user could
                        // customize the encoding which we will loose, such as ThrowOnError on UTF8
                        // Note: we don't need to recompute readToUserBuffer optimization as IsPreamble
                        // doesn't change the encoding or affect _maxCharsPerBuffer
                        if (IsPreamble())
                        {
                            continue;
                        }

                        // On the first call to ReadBuffer, if we're supposed to detect the encoding, do it.
                        if (_detectEncoding && _byteLen >= 2)
                        {
                            DetectEncoding();
                            // DetectEncoding changes some buffer state.  Recompute this.
                            readToUserBuffer = count >= _maxCharsPerBuffer;
                        }

                        Debug.Assert(n == 0);

                        _charPos = 0;
                        if (readToUserBuffer)
                        {
                            n = _decoder.GetChars(new ReadOnlySpan<byte>(tmpByteBuffer, 0, _byteLen), buffer.Span.Slice(charsRead), flush: false);
                            _charLen = 0;  // StreamReader's buffer is empty.
                        }
                        else
                        {
                            n = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, 0);
                            _charLen += n;  // Number of chars in StreamReader's buffer.
                        }
                    } while (n == 0);

                    if (n == 0)
                    {
                        break;  // We're at EOF
                    }
                }  // if (n == 0)

                // Got more chars in charBuffer than the user requested
                if (n > count)
                {
                    n = count;
                }

                if (!readToUserBuffer)
                {
                    new Span<char>(_charBuffer, _charPos, n).CopyTo(buffer.Span.Slice(charsRead));
                    _charPos += n;
                }

                charsRead += n;
                count -= n;

                // This function shouldn't block for an indefinite amount of time,
                // or reading from a network stream won't work right.  If we got
                // fewer bytes than we requested, then we want to break right here.
                if (_isBlocked)
                {
                    break;
                }
            }  // while (count > 0)

            return charsRead;
        }

        public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(buffer);

            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (buffer.Length - index < count)
            {
                throw new ArgumentException(SR.Argument_InvalidOffLen);
            }

            // If we have been inherited into a subclass, the following implementation could be incorrect
            // since it does not call through to Read() which a subclass might have overridden.
            // To be safe we will only use this implementation in cases where we know it is safe to do so,
            // and delegate to our base class (which will call into Read) when we are not sure.
            if (GetType() != typeof(StreamReader))
            {
                return base.ReadBlockAsync(buffer, index, count);
            }

            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (RuntimeHelpers.IsRuntimeAsync())
            {
                return ReadBlockAsyncWithGuard(buffer, index, count);
            }

            Task<int> task = base.ReadBlockAsync(buffer, index, count);
            _asyncReadTask = task;
            return task;

            async Task<int> ReadBlockAsyncWithGuard(char[] buffer, int index, int count)
            {
                using ThrowOnReadsScope _ = GuardAgainstOtherReads();
                return await base.ReadBlockAsync(buffer, index, count).ConfigureAwait(false);
            }
        }

        public override ValueTask<int> ReadBlockAsync(Memory<char> buffer, CancellationToken cancellationToken = default)
        {
            if (GetType() != typeof(StreamReader))
            {
                // If a derived type may have overridden ReadBlockAsync(char[], ...) before this overload
                // was introduced, defer to it.
                return base.ReadBlockAsync(buffer, cancellationToken);
            }

            ThrowIfDisposed();
            CheckAsyncTaskInProgress();

            if (cancellationToken.IsCancellationRequested)
            {
                return ValueTask.FromCanceled<int>(cancellationToken);
            }

            if (RuntimeHelpers.IsRuntimeAsync())
            {
                return ReadBlockAsyncInternalWithGuard(buffer, cancellationToken);
            }

            ValueTask<int> vt = ReadBlockAsyncInternal(buffer, cancellationToken);
            if (vt.IsCompletedSuccessfully)
            {
                return vt;
            }

            Task<int> t = vt.AsTask();
            _asyncReadTask = t;
            return new ValueTask<int>(t);

            async ValueTask<int> ReadBlockAsyncInternalWithGuard(Memory<char> buffer, CancellationToken token)
            {
                using ThrowOnReadsScope _ = GuardAgainstOtherReads();
                return await ReadBlockAsyncInternal(buffer, token).ConfigureAwait(false);
            }
        }

        private async ValueTask<int> ReadBufferAsync(CancellationToken cancellationToken)
        {
            _charLen = 0;
            _charPos = 0;
            byte[] tmpByteBuffer = _byteBuffer;
            Stream tmpStream = _stream;

            if (!_checkPreamble)
            {
                _byteLen = 0;
            }

            bool eofReached = false;

            do
            {
                if (_checkPreamble)
                {
                    Debug.Assert(_bytePos <= _encoding.Preamble.Length, "possible bug in _compressPreamble. Are two threads using this StreamReader at the same time?");
                    int tmpBytePos = _bytePos;
                    int len = await tmpStream.ReadAsync(tmpByteBuffer.AsMemory(tmpBytePos), cancellationToken).ConfigureAwait(false);
                    Debug.Assert(len >= 0, "Stream.Read returned a negative number!  This is a bug in your stream class.");

                    if (len == 0)
                    {
                        eofReached = true;
                        break;
                    }

                    _byteLen += len;
                }
                else
                {
                    Debug.Assert(_bytePos == 0, "_bytePos can be non zero only when we are trying to _checkPreamble. Are two threads using this StreamReader at the same time?");
                    _byteLen = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer), cancellationToken).ConfigureAwait(false);
                    Debug.Assert(_byteLen >= 0, "Stream.Read returned a negative number!  Bug in stream class.");

                    if (_byteLen == 0)
                    {
                        eofReached = true;
                        break;
                    }
                }

                // _isBlocked == whether we read fewer bytes than we asked for.
                // Note we must check it here because CompressBuffer or
                // DetectEncoding will change _byteLen.
                _isBlocked = (_byteLen < tmpByteBuffer.Length);

                // Check for preamble before detect encoding. This is not to override the
                // user supplied Encoding for the one we implicitly detect. The user could
                // customize the encoding which we will loose, such as ThrowOnError on UTF8
                if (IsPreamble())
                {
                    continue;
                }

                // If we're supposed to detect the encoding and haven't done so yet,
                // do it.  Note this may need to be called more than once.
                if (_detectEncoding && _byteLen >= 2)
                {
                    DetectEncoding();
                }

                Debug.Assert(_charPos == 0 && _charLen == 0, "We shouldn't be trying to decode more data if we made progress in an earlier iteration.");
                _charLen = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, 0, flush: false);
            } while (_charLen == 0);

            if (eofReached)
            {
                // EOF has been reached - perform final flush.
                // We need to reset _bytePos and _byteLen just in case we hadn't
                // finished processing the preamble before we reached EOF.

                Debug.Assert(_charPos == 0 && _charLen == 0, "We shouldn't be looking for EOF unless we have an empty char buffer.");
                _charLen = _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, 0, flush: true);
                _bytePos = 0;
                _byteLen = 0;
            }

            return _charLen;
        }

        private void ThrowIfDisposed()
        {
            if (_disposed)
            {
                ThrowObjectDisposedException();
            }

            void ThrowObjectDisposedException() => throw new ObjectDisposedException(GetType().Name, SR.ObjectDisposed_ReaderClosed);
        }

        // No data, class doesn't need to be serializable.
        // Note this class is threadsafe.
        internal sealed class NullStreamReader : StreamReader
        {
            public override Encoding CurrentEncoding => Encoding.Unicode;

            protected override void Dispose(bool disposing)
            {
                // Do nothing - this is essentially unclosable.
            }

            public override int Peek() => -1;

            public override int Read() => -1;

            public override int Read(char[] buffer, int index, int count) => 0;

            public override int Read(Span<char> buffer) => 0;

            public override Task<int> ReadAsync(char[] buffer, int index, int count) => Task.FromResult(0);

            public override ValueTask<int> ReadAsync(Memory<char> buffer, CancellationToken cancellationToken) =>
                cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled<int>(cancellationToken) : default;

            public override int ReadBlock(char[] buffer, int index, int count) => 0;

            public override int ReadBlock(Span<char> buffer) => 0;

            public override Task<int> ReadBlockAsync(char[] buffer, int index, int count) => Task.FromResult(0);

            public override ValueTask<int> ReadBlockAsync(Memory<char> buffer, CancellationToken cancellationToken) =>
                cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled<int>(cancellationToken) : default;

            public override string? ReadLine() => null;

            public override Task<string?> ReadLineAsync() => Task.FromResult<string?>(null);

            public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken) =>
                cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled<string?>(cancellationToken) : default;

            public override string ReadToEnd() => "";

            public override Task<string> ReadToEndAsync() => Task.FromResult("");

            public override Task<string> ReadToEndAsync(CancellationToken cancellationToken) =>
                cancellationToken.IsCancellationRequested ? Task.FromCanceled<string>(cancellationToken) : Task.FromResult("");

            internal override ValueTask<int> ReadAsyncInternal(Memory<char> buffer, CancellationToken cancellationToken) =>
                cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled<int>(cancellationToken) : default;

            internal override int ReadBuffer() => 0;
        }
    }
}