File: System\Text\EncodingExtensions.cs
Web Access
Project: src\src\libraries\System.Memory\src\System.Memory.csproj (System.Memory)
// 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.Collections.Generic;
using System.Diagnostics;
 
namespace System.Text
{
    public static class EncodingExtensions
    {
        /// <summary>
        /// The maximum number of input elements after which we'll begin to chunk the input.
        /// </summary>
        /// <remarks>
        /// The reason for this chunking is that the existing Encoding / Encoder / Decoder APIs
        /// like GetByteCount / GetCharCount will throw if an integer overflow occurs. Since
        /// we may be working with large inputs in these extension methods, we don't want to
        /// risk running into this issue. While it's technically possible even for 1 million
        /// input elements to result in an overflow condition, such a scenario is unrealistic,
        /// so we won't worry about it.
        /// </remarks>
        private const int MaxInputElementsPerIteration = 1 * 1024 * 1024;
 
        /// <summary>
        /// Encodes the specified <see cref="ReadOnlySpan{Char}"/> to <see langword="byte"/>s using the specified <see cref="Encoding"/>
        /// and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="chars"/> should be encoded.</param>
        /// <param name="chars">The <see cref="ReadOnlySpan{Char}"/> to encode to <see langword="byte"/>s.</param>
        /// <param name="writer">The buffer to which the encoded bytes will be written.</param>
        /// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static long GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, IBufferWriter<byte> writer)
        {
            ArgumentNullException.ThrowIfNull(encoding);
            ArgumentNullException.ThrowIfNull(writer);
 
            if (chars.Length <= MaxInputElementsPerIteration)
            {
                // The input span is small enough where we can one-shot this.
 
                int byteCount = encoding.GetByteCount(chars);
                Span<byte> scratchBuffer = writer.GetSpan(byteCount);
 
                int actualBytesWritten = encoding.GetBytes(chars, scratchBuffer);
 
                writer.Advance(actualBytesWritten);
                return actualBytesWritten;
            }
            else
            {
                // Allocate a stateful Encoder instance and chunk this.
 
                Convert(encoding.GetEncoder(), chars, writer, flush: true, out long totalBytesWritten, out _);
                return totalBytesWritten;
            }
        }
 
        /// <summary>
        /// Decodes the specified <see cref="ReadOnlySequence{Char}"/> to <see langword="byte"/>s using the specified <see cref="Encoding"/>
        /// and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="chars"/> should be encoded.</param>
        /// <param name="chars">The <see cref="ReadOnlySequence{Char}"/> whose contents should be encoded.</param>
        /// <param name="writer">The buffer to which the encoded bytes will be written.</param>
        /// <returns>The number of bytes written to <paramref name="writer"/>.</returns>
        /// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static long GetBytes(this Encoding encoding, in ReadOnlySequence<char> chars, IBufferWriter<byte> writer)
        {
            ArgumentNullException.ThrowIfNull(encoding);
            ArgumentNullException.ThrowIfNull(writer);
 
            // Delegate to the Span-based method if possible.
            // If that doesn't work, allocate the Encoder instance and run a loop.
 
            if (chars.IsSingleSegment)
            {
                return GetBytes(encoding, chars.FirstSpan, writer);
            }
            else
            {
                Convert(encoding.GetEncoder(), chars, writer, flush: true, out long bytesWritten, out _);
                return bytesWritten;
            }
        }
 
        /// <summary>
        /// Encodes the specified <see cref="ReadOnlySequence{Char}"/> to <see langword="byte"/>s using the specified <see cref="Encoding"/>
        /// and outputs the result to <paramref name="bytes"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="chars"/> should be encoded.</param>
        /// <param name="chars">The <see cref="ReadOnlySequence{Char}"/> to encode to <see langword="byte"/>s.</param>
        /// <param name="bytes">The destination buffer to which the encoded bytes will be written.</param>
        /// <returns>The number of bytes written to <paramref name="bytes"/>.</returns>
        /// <exception cref="ArgumentException">Thrown if <paramref name="bytes"/> is not large enough to contain the encoded form of <paramref name="chars"/>.</exception>
        /// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static int GetBytes(this Encoding encoding, in ReadOnlySequence<char> chars, Span<byte> bytes)
        {
            ArgumentNullException.ThrowIfNull(encoding);
 
            if (chars.IsSingleSegment)
            {
                // If the incoming sequence is single-segment, one-shot this.
 
                return encoding.GetBytes(chars.FirstSpan, bytes);
            }
            else
            {
                // If the incoming sequence is multi-segment, create a stateful Encoder
                // and use it as the workhorse. On the final iteration we'll pass flush=true.
 
                ReadOnlySequence<char> remainingChars = chars;
                int originalBytesLength = bytes.Length;
                Encoder encoder = encoding.GetEncoder();
                bool isFinalSegment;
 
                do
                {
                    remainingChars.GetFirstSpan(out ReadOnlySpan<char> firstSpan, out SequencePosition next);
                    isFinalSegment = remainingChars.IsSingleSegment;
 
                    int bytesWrittenJustNow = encoder.GetBytes(firstSpan, bytes, flush: isFinalSegment);
                    bytes = bytes.Slice(bytesWrittenJustNow);
                    remainingChars = remainingChars.Slice(next);
                } while (!isFinalSegment);
 
                return originalBytesLength - bytes.Length; // total number of bytes we wrote
            }
        }
 
        /// <summary>
        /// Encodes the specified <see cref="ReadOnlySequence{Char}"/> into a <see cref="byte"/> array using the specified <see cref="Encoding"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="chars"/> should be encoded.</param>
        /// <param name="chars">The <see cref="ReadOnlySequence{Char}"/> to encode to <see langword="byte"/>s.</param>
        /// <returns>A <see cref="byte"/> array which represents the encoded contents of <paramref name="chars"/>.</returns>
        /// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static byte[] GetBytes(this Encoding encoding, in ReadOnlySequence<char> chars)
        {
            ArgumentNullException.ThrowIfNull(encoding);
 
            if (chars.IsSingleSegment)
            {
                // If the incoming sequence is single-segment, one-shot this.
 
                ReadOnlySpan<char> span = chars.FirstSpan;
 
                byte[] retVal = new byte[encoding.GetByteCount(span)];
                encoding.GetBytes(span, retVal);
                return retVal;
            }
            else
            {
                // If the incoming sequence is multi-segment, create a stateful Encoder
                // and use it as the workhorse. On the final iteration we'll pass flush=true.
 
                Encoder encoder = encoding.GetEncoder();
 
                // Maintain a list of all the segments we'll need to concat together.
                // These will be released back to the pool at the end of the method.
 
                List<(byte[], int)> listOfSegments = new List<(byte[], int)>();
                int totalByteCount = 0;
 
                ReadOnlySequence<char> remainingChars = chars;
                bool isFinalSegment;
 
                do
                {
                    remainingChars.GetFirstSpan(out ReadOnlySpan<char> firstSpan, out SequencePosition next);
                    isFinalSegment = remainingChars.IsSingleSegment;
 
                    int byteCountThisIteration = encoder.GetByteCount(firstSpan, flush: isFinalSegment);
                    byte[] rentedArray = ArrayPool<byte>.Shared.Rent(byteCountThisIteration);
                    int actualBytesWrittenThisIteration = encoder.GetBytes(firstSpan, rentedArray, flush: isFinalSegment); // could throw ArgumentException if overflow would occur
                    listOfSegments.Add((rentedArray, actualBytesWrittenThisIteration));
 
                    totalByteCount += actualBytesWrittenThisIteration;
                    if (totalByteCount < 0)
                    {
                        // If we overflowed, call the array ctor, passing int.MaxValue.
                        // This will end up throwing the expected OutOfMemoryException
                        // since arrays are limited to under int.MaxValue elements in length.
 
                        totalByteCount = int.MaxValue;
                        break;
                    }
 
                    remainingChars = remainingChars.Slice(next);
                } while (!isFinalSegment);
 
                // Now build up the byte[] to return, then release all of our scratch buffers
                // back to the shared pool.
 
                byte[] retVal = new byte[totalByteCount];
                Span<byte> remainingBytes = retVal;
 
                foreach ((byte[] array, int length) in listOfSegments)
                {
                    array.AsSpan(0, length).CopyTo(remainingBytes);
                    ArrayPool<byte>.Shared.Return(array);
                    remainingBytes = remainingBytes.Slice(length);
                }
 
                Debug.Assert(remainingBytes.IsEmpty, "Over-allocated the byte[] instance?");
 
                return retVal;
            }
        }
 
        /// <summary>
        /// Decodes the specified <see cref="ReadOnlySpan{Byte}"/> to <see langword="char"/>s using the specified <see cref="Encoding"/>
        /// and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="bytes"/> should be decoded.</param>
        /// <param name="bytes">The <see cref="ReadOnlySpan{Byte}"/> whose bytes should be decoded.</param>
        /// <param name="writer">The buffer to which the decoded chars will be written.</param>
        /// <returns>The number of chars written to <paramref name="writer"/>.</returns>
        /// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be decoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static long GetChars(this Encoding encoding, ReadOnlySpan<byte> bytes, IBufferWriter<char> writer)
        {
            ArgumentNullException.ThrowIfNull(encoding);
            ArgumentNullException.ThrowIfNull(writer);
 
            if (bytes.Length <= MaxInputElementsPerIteration)
            {
                // The input span is small enough where we can one-shot this.
 
                int charCount = encoding.GetCharCount(bytes);
                Span<char> scratchBuffer = writer.GetSpan(charCount);
 
                int actualCharsWritten = encoding.GetChars(bytes, scratchBuffer);
 
                writer.Advance(actualCharsWritten);
                return actualCharsWritten;
            }
            else
            {
                // Allocate a stateful Decoder instance and chunk this.
 
                Convert(encoding.GetDecoder(), bytes, writer, flush: true, out long totalCharsWritten, out _);
                return totalCharsWritten;
            }
        }
 
        /// <summary>
        /// Decodes the specified <see cref="ReadOnlySequence{Byte}"/> to <see langword="char"/>s using the specified <see cref="Encoding"/>
        /// and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="bytes"/> should be decoded.</param>
        /// <param name="bytes">The <see cref="ReadOnlySequence{Byte}"/> whose bytes should be decoded.</param>
        /// <param name="writer">The buffer to which the decoded chars will be written.</param>
        /// <returns>The number of chars written to <paramref name="writer"/>.</returns>
        /// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be decoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static long GetChars(this Encoding encoding, in ReadOnlySequence<byte> bytes, IBufferWriter<char> writer)
        {
            ArgumentNullException.ThrowIfNull(encoding);
            ArgumentNullException.ThrowIfNull(writer);
 
            // Delegate to the Span-based method if possible.
            // If that doesn't work, allocate the Encoder instance and run a loop.
 
            if (bytes.IsSingleSegment)
            {
                return GetChars(encoding, bytes.FirstSpan, writer);
            }
            else
            {
                Convert(encoding.GetDecoder(), bytes, writer, flush: true, out long charsWritten, out _);
                return charsWritten;
            }
        }
 
        /// <summary>
        /// Decodes the specified <see cref="ReadOnlySequence{Byte}"/> to <see langword="char"/>s using the specified <see cref="Encoding"/>
        /// and outputs the result to <paramref name="chars"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="bytes"/> is encoded.</param>
        /// <param name="bytes">The <see cref="ReadOnlySequence{Byte}"/> to decode to characters.</param>
        /// <param name="chars">The destination buffer to which the decoded characters will be written.</param>
        /// <returns>The number of chars written to <paramref name="chars"/>.</returns>
        /// <exception cref="ArgumentException">Thrown if <paramref name="chars"/> is not large enough to contain the encoded form of <paramref name="bytes"/>.</exception>
        /// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be decoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static int GetChars(this Encoding encoding, in ReadOnlySequence<byte> bytes, Span<char> chars)
        {
            ArgumentNullException.ThrowIfNull(encoding);
 
            if (bytes.IsSingleSegment)
            {
                // If the incoming sequence is single-segment, one-shot this.
 
                return encoding.GetChars(bytes.FirstSpan, chars);
            }
            else
            {
                // If the incoming sequence is multi-segment, create a stateful Decoder
                // and use it as the workhorse. On the final iteration we'll pass flush=true.
 
                ReadOnlySequence<byte> remainingBytes = bytes;
                int originalCharsLength = chars.Length;
                Decoder decoder = encoding.GetDecoder();
                bool isFinalSegment;
 
                do
                {
                    remainingBytes.GetFirstSpan(out ReadOnlySpan<byte> firstSpan, out SequencePosition next);
                    isFinalSegment = remainingBytes.IsSingleSegment;
 
                    int charsWrittenJustNow = decoder.GetChars(firstSpan, chars, flush: isFinalSegment);
                    chars = chars.Slice(charsWrittenJustNow);
                    remainingBytes = remainingBytes.Slice(next);
                } while (!isFinalSegment);
 
                return originalCharsLength - chars.Length; // total number of chars we wrote
            }
        }
 
        /// <summary>
        /// Decodes the specified <see cref="ReadOnlySequence{Byte}"/> into a <see cref="string"/> using the specified <see cref="Encoding"/>.
        /// </summary>
        /// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="bytes"/> is encoded.</param>
        /// <param name="bytes">The <see cref="ReadOnlySequence{Byte}"/> to decode into characters.</param>
        /// <returns>A <see cref="string"/> which represents the decoded contents of <paramref name="bytes"/>.</returns>
        /// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be decoded and <paramref name="encoding"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static string GetString(this Encoding encoding, in ReadOnlySequence<byte> bytes)
        {
            ArgumentNullException.ThrowIfNull(encoding);
 
            if (bytes.IsSingleSegment)
            {
                // If the incoming sequence is single-segment, one-shot this.
 
                return encoding.GetString(bytes.FirstSpan);
            }
            else
            {
                // If the incoming sequence is multi-segment, create a stateful Decoder
                // and use it as the workhorse. On the final iteration we'll pass flush=true.
 
                Decoder decoder = encoding.GetDecoder();
 
                // Maintain a list of all the segments we'll need to concat together.
                // These will be released back to the pool at the end of the method.
 
                List<(char[], int)> listOfSegments = new List<(char[], int)>();
                int totalCharCount = 0;
 
                ReadOnlySequence<byte> remainingBytes = bytes;
                bool isFinalSegment;
 
                do
                {
                    remainingBytes.GetFirstSpan(out ReadOnlySpan<byte> firstSpan, out SequencePosition next);
                    isFinalSegment = remainingBytes.IsSingleSegment;
 
                    int charCountThisIteration = decoder.GetCharCount(firstSpan, flush: isFinalSegment); // could throw ArgumentException if overflow would occur
                    char[] rentedArray = ArrayPool<char>.Shared.Rent(charCountThisIteration);
                    int actualCharsWrittenThisIteration = decoder.GetChars(firstSpan, rentedArray, flush: isFinalSegment);
                    listOfSegments.Add((rentedArray, actualCharsWrittenThisIteration));
 
                    totalCharCount += actualCharsWrittenThisIteration;
                    if (totalCharCount < 0)
                    {
                        // If we overflowed, call string.Create, passing int.MaxValue.
                        // This will end up throwing the expected OutOfMemoryException
                        // since strings are limited to under int.MaxValue elements in length.
 
                        totalCharCount = int.MaxValue;
                        break;
                    }
 
                    remainingBytes = remainingBytes.Slice(next);
                } while (!isFinalSegment);
 
                // Now build up the string to return, then release all of our scratch buffers
                // back to the shared pool.
 
                return string.Create(totalCharCount, listOfSegments, (span, listOfSegments) =>
                {
                    foreach ((char[] array, int length) in listOfSegments)
                    {
                        array.AsSpan(0, length).CopyTo(span);
                        ArrayPool<char>.Shared.Return(array);
                        span = span.Slice(length);
                    }
 
                    Debug.Assert(span.IsEmpty, "Over-allocated the string instance?");
                });
            }
        }
 
        /// <summary>
        /// Converts a <see cref="ReadOnlySpan{Char}"/> to bytes using <paramref name="encoder"/> and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="encoder">The <see cref="Encoder"/> instance which can convert <see langword="char"/>s to <see langword="byte"/>s.</param>
        /// <param name="chars">A sequence of characters to encode.</param>
        /// <param name="writer">The buffer to which the encoded bytes will be written.</param>
        /// <param name="flush"><see langword="true"/> to indicate no further data is to be converted; otherwise <see langword="false"/>.</param>
        /// <param name="bytesUsed">When this method returns, contains the count of <see langword="byte"/>s which were written to <paramref name="writer"/>.</param>
        /// <param name="completed">
        /// When this method returns, contains <see langword="true"/> if <paramref name="encoder"/> contains no partial internal state; otherwise, <see langword="false"/>.
        /// If <paramref name="flush"/> is <see langword="true"/>, this will always be set to <see langword="true"/> when the method returns.
        /// </param>
        /// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoder"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static void Convert(this Encoder encoder, ReadOnlySpan<char> chars, IBufferWriter<byte> writer, bool flush, out long bytesUsed, out bool completed)
        {
            ArgumentNullException.ThrowIfNull(encoder);
            ArgumentNullException.ThrowIfNull(writer);
 
            // We need to perform at least one iteration of the loop since the encoder could have internal state.
 
            long totalBytesWritten = 0;
 
            do
            {
                // If our remaining input is very large, instead truncate it and tell the encoder
                // that there'll be more data after this call. This truncation is only for the
                // purposes of getting the required byte count. Since the writer may give us a span
                // larger than what we asked for, we'll pass the entirety of the remaining data
                // to the transcoding routine, since it may be able to make progress beyond what
                // was initially computed for the truncated input data.
 
                int byteCountForThisSlice = (chars.Length <= MaxInputElementsPerIteration)
                  ? encoder.GetByteCount(chars, flush)
                  : encoder.GetByteCount(chars.Slice(0, MaxInputElementsPerIteration), flush: false /* this isn't the end of the data */);
 
                Span<byte> scratchBuffer = writer.GetSpan(byteCountForThisSlice);
 
                encoder.Convert(chars, scratchBuffer, flush, out int charsUsedJustNow, out int bytesWrittenJustNow, out completed);
 
                chars = chars.Slice(charsUsedJustNow);
                writer.Advance(bytesWrittenJustNow);
                totalBytesWritten += bytesWrittenJustNow;
            } while (!chars.IsEmpty);
 
            bytesUsed = totalBytesWritten;
        }
 
        /// <summary>
        /// Converts a <see cref="ReadOnlySequence{Char}"/> to encoded bytes and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="encoder">The <see cref="Encoder"/> instance which can convert <see langword="char"/>s to <see langword="byte"/>s.</param>
        /// <param name="chars">A sequence of characters to encode.</param>
        /// <param name="writer">The buffer to which the encoded bytes will be written.</param>
        /// <param name="flush"><see langword="true"/> to indicate no further data is to be converted; otherwise <see langword="false"/>.</param>
        /// <param name="bytesUsed">When this method returns, contains the count of <see langword="byte"/>s which were written to <paramref name="writer"/>.</param>
        /// <param name="completed">When this method returns, contains <see langword="true"/> if all input up until <paramref name="bytesUsed"/> was
        /// converted; otherwise, <see langword="false"/>. If <paramref name="flush"/> is <see langword="true"/>, this will always be set to
        /// <see langword="true"/> when the method returns.</param>
        /// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoder"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static void Convert(this Encoder encoder, in ReadOnlySequence<char> chars, IBufferWriter<byte> writer, bool flush, out long bytesUsed, out bool completed)
        {
            // Parameter null checks will be performed by the workhorse routine.
 
            if (chars.IsSingleSegment)
            {
                Convert(encoder, chars.FirstSpan, writer, flush, out bytesUsed, out completed);
            }
            else
            {
                ReadOnlySequence<char> remainingChars = chars;
                long totalBytesWritten = 0;
                bool isFinalSegment;
 
                do
                {
                    // Process each segment individually. We need to run at least one iteration of the loop in case
                    // the Encoder has internal state.
 
                    remainingChars.GetFirstSpan(out ReadOnlySpan<char> firstSpan, out SequencePosition next);
                    isFinalSegment = remainingChars.IsSingleSegment;
 
                    Convert(encoder, firstSpan, writer, flush && isFinalSegment, out long bytesWrittenThisIteration, out completed);
 
                    totalBytesWritten += bytesWrittenThisIteration;
                    remainingChars = remainingChars.Slice(next);
                } while (!isFinalSegment);
 
                bytesUsed = totalBytesWritten;
            }
        }
 
        /// <summary>
        /// Converts a <see cref="ReadOnlySpan{Byte}"/> to chars using <paramref name="decoder"/> and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="decoder">The <see cref="Decoder"/> instance which can convert <see langword="byte"/>s to <see langword="char"/>s.</param>
        /// <param name="bytes">A sequence of bytes to decode.</param>
        /// <param name="writer">The buffer to which the decoded chars will be written.</param>
        /// <param name="flush"><see langword="true"/> to indicate no further data is to be converted; otherwise <see langword="false"/>.</param>
        /// <param name="charsUsed">When this method returns, contains the count of <see langword="char"/>s which were written to <paramref name="writer"/>.</param>
        /// <param name="completed">
        /// When this method returns, contains <see langword="true"/> if <paramref name="decoder"/> contains no partial internal state; otherwise, <see langword="false"/>.
        /// If <paramref name="flush"/> is <see langword="true"/>, this will always be set to <see langword="true"/> when the method returns.
        /// </param>
        /// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be encoded and <paramref name="decoder"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static void Convert(this Decoder decoder, ReadOnlySpan<byte> bytes, IBufferWriter<char> writer, bool flush, out long charsUsed, out bool completed)
        {
            ArgumentNullException.ThrowIfNull(decoder);
            ArgumentNullException.ThrowIfNull(writer);
 
            // We need to perform at least one iteration of the loop since the decoder could have internal state.
 
            long totalCharsWritten = 0;
 
            do
            {
                // If our remaining input is very large, instead truncate it and tell the decoder
                // that there'll be more data after this call. This truncation is only for the
                // purposes of getting the required char count. Since the writer may give us a span
                // larger than what we asked for, we'll pass the entirety of the remaining data
                // to the transcoding routine, since it may be able to make progress beyond what
                // was initially computed for the truncated input data.
 
                int charCountForThisSlice = (bytes.Length <= MaxInputElementsPerIteration)
                    ? decoder.GetCharCount(bytes, flush)
                    : decoder.GetCharCount(bytes.Slice(0, MaxInputElementsPerIteration), flush: false /* this isn't the end of the data */);
 
                Span<char> scratchBuffer = writer.GetSpan(charCountForThisSlice);
 
                decoder.Convert(bytes, scratchBuffer, flush, out int bytesUsedJustNow, out int charsWrittenJustNow, out completed);
 
                bytes = bytes.Slice(bytesUsedJustNow);
                writer.Advance(charsWrittenJustNow);
                totalCharsWritten += charsWrittenJustNow;
            } while (!bytes.IsEmpty);
 
            charsUsed = totalCharsWritten;
        }
 
        /// <summary>
        /// Converts a <see cref="ReadOnlySequence{Byte}"/> to UTF-16 encoded characters and writes the result to <paramref name="writer"/>.
        /// </summary>
        /// <param name="decoder">The <see cref="Decoder"/> instance which can convert <see langword="byte"/>s to <see langword="char"/>s.</param>
        /// <param name="bytes">A sequence of bytes to decode.</param>
        /// <param name="writer">The buffer to which the decoded characters will be written.</param>
        /// <param name="flush"><see langword="true"/> to indicate no further data is to be converted; otherwise <see langword="false"/>.</param>
        /// <param name="charsUsed">When this method returns, contains the count of <see langword="char"/>s which were written to <paramref name="writer"/>.</param>
        /// <param name="completed">
        /// When this method returns, contains <see langword="true"/> if <paramref name="decoder"/> contains no partial internal state; otherwise, <see langword="false"/>.
        /// If <paramref name="flush"/> is <see langword="true"/>, this will always be set to <see langword="true"/> when the method returns.
        /// </param>
        /// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be decoded and <paramref name="decoder"/> is configured
        /// to throw an exception when such data is seen.</exception>
        public static void Convert(this Decoder decoder, in ReadOnlySequence<byte> bytes, IBufferWriter<char> writer, bool flush, out long charsUsed, out bool completed)
        {
            // Parameter null checks will be performed by the workhorse routine.
 
            if (bytes.IsSingleSegment)
            {
                Convert(decoder, bytes.FirstSpan, writer, flush, out charsUsed, out completed);
            }
            else
            {
                ReadOnlySequence<byte> remainingBytes = bytes;
                long totalCharsWritten = 0;
                bool isFinalSegment;
 
                do
                {
                    // Process each segment individually. We need to run at least one iteration of the loop in case
                    // the Decoder has internal state.
 
                    remainingBytes.GetFirstSpan(out ReadOnlySpan<byte> firstSpan, out SequencePosition next);
                    isFinalSegment = remainingBytes.IsSingleSegment;
 
                    Convert(decoder, firstSpan, writer, flush && isFinalSegment, out long charsWrittenThisIteration, out completed);
 
                    totalCharsWritten += charsWrittenThisIteration;
                    remainingBytes = remainingBytes.Slice(next);
                } while (!isFinalSegment);
 
                charsUsed = totalCharsWritten;
            }
        }
    }
}