File: src\libraries\System.Private.CoreLib\src\System\IO\BinaryWriter.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\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.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
 
namespace System.IO
{
    // This abstract base class represents a writer that can write
    // primitives to an arbitrary stream. A subclass can override methods to
    // give unique encodings.
    //
    public class BinaryWriter : IDisposable, IAsyncDisposable
    {
        private const int MaxArrayPoolRentalSize = 64 * 1024; // try to keep rentals to a reasonable size
 
        public static readonly BinaryWriter Null = new BinaryWriter();
 
        protected Stream OutStream;
        private readonly Encoding _encoding;
        private readonly bool _leaveOpen;
        private readonly bool _useFastUtf8;
 
        // Protected default constructor that sets the output stream
        // to a null stream (a bit bucket).
        protected BinaryWriter()
        {
            OutStream = Stream.Null;
            _encoding = Encoding.UTF8;
            _useFastUtf8 = true;
        }
 
        // BinaryWriter never emits a BOM, so can use Encoding.UTF8 fast singleton
        public BinaryWriter(Stream output) : this(output, Encoding.UTF8, false)
        {
        }
 
        public BinaryWriter(Stream output, Encoding encoding) : this(output, encoding, false)
        {
        }
 
        public BinaryWriter(Stream output, Encoding encoding, bool leaveOpen)
        {
            ArgumentNullException.ThrowIfNull(output);
            ArgumentNullException.ThrowIfNull(encoding);
 
            if (!output.CanWrite)
                throw new ArgumentException(SR.Argument_StreamNotWritable);
 
            OutStream = output;
            _encoding = encoding;
            _leaveOpen = leaveOpen;
            _useFastUtf8 = encoding.IsUTF8CodePage && encoding.EncoderFallback.MaxCharCount <= 1;
        }
 
        // Closes this writer and releases any system resources associated with the
        // writer. Following a call to Close, any operations on the writer
        // may raise exceptions.
        public virtual void Close()
        {
            Dispose(true);
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_leaveOpen)
                    OutStream.Flush();
                else
                    OutStream.Close();
            }
        }
 
        public void Dispose()
        {
            Dispose(true);
        }
 
        public virtual ValueTask DisposeAsync()
        {
            try
            {
                if (GetType() == typeof(BinaryWriter))
                {
                    if (_leaveOpen)
                    {
                        return new ValueTask(OutStream.FlushAsync());
                    }
 
                    OutStream.Close();
                }
                else
                {
                    // Since this is a derived BinaryWriter, delegate to whatever logic
                    // the derived implementation already has in Dispose.
                    Dispose();
                }
 
                return default;
            }
            catch (Exception exc)
            {
                return ValueTask.FromException(exc);
            }
        }
 
        // Returns the stream associated with the writer. It flushes all pending
        // writes before returning. All subclasses should override Flush to
        // ensure that all buffered data is sent to the stream.
        public virtual Stream BaseStream
        {
            get
            {
                Flush();
                return OutStream;
            }
        }
 
        // Clears all buffers for this writer and causes any buffered data to be
        // written to the underlying device.
        public virtual void Flush()
        {
            OutStream.Flush();
        }
 
        public virtual long Seek(int offset, SeekOrigin origin)
        {
            return OutStream.Seek(offset, origin);
        }
 
        // Writes a boolean to this stream. A single byte is written to the stream
        // with the value 0 representing false or the value 1 representing true.
        //
        public virtual void Write(bool value) => OutStream.WriteByte((byte)(value ? 1 : 0));
 
        // Writes a byte to this stream. The current position of the stream is
        // advanced by one.
        //
        public virtual void Write(byte value) => OutStream.WriteByte(value);
 
        // Writes a signed byte to this stream. The current position of the stream
        // is advanced by one.
        //
        [CLSCompliant(false)]
        public virtual void Write(sbyte value) => OutStream.WriteByte((byte)value);
 
        // Writes a byte array to this stream.
        //
        // This default implementation calls the Write(Object, int, int)
        // method to write the byte array.
        //
        public virtual void Write(byte[] buffer)
        {
            ArgumentNullException.ThrowIfNull(buffer);
 
            OutStream.Write(buffer, 0, buffer.Length);
        }
 
        // Writes a section of a byte array to this stream.
        //
        // This default implementation calls the Write(Object, int, int)
        // method to write the byte array.
        //
        public virtual void Write(byte[] buffer, int index, int count)
        {
            OutStream.Write(buffer, index, count);
        }
 
        // Writes a character to this stream. The current position of the stream is
        // advanced by two.
        // Note this method cannot handle surrogates properly in UTF-8.
        //
        public virtual void Write(char ch)
        {
            if (!Rune.TryCreate(ch, out Rune rune)) // optimistically assume UTF-8 code path (which uses Rune) will be hit
            {
                throw new ArgumentException(SR.Arg_SurrogatesNotAllowedAsSingleChar);
            }
 
            Span<byte> buffer = stackalloc byte[8]; // reasonable guess for worst-case expansion for any arbitrary encoding
 
            if (_useFastUtf8)
            {
                int utf8ByteCount = rune.EncodeToUtf8(buffer);
                OutStream.Write(buffer.Slice(0, utf8ByteCount));
            }
            else
            {
                byte[]? rented = null;
                int maxByteCount = _encoding.GetMaxByteCount(1);
 
                if (maxByteCount > buffer.Length)
                {
                    rented = ArrayPool<byte>.Shared.Rent(maxByteCount);
                    buffer = rented;
                }
 
                int actualByteCount = _encoding.GetBytes(new ReadOnlySpan<char>(in ch), buffer);
                OutStream.Write(buffer.Slice(0, actualByteCount));
 
                if (rented != null)
                {
                    ArrayPool<byte>.Shared.Return(rented);
                }
            }
        }
 
        // Writes a character array to this stream.
        //
        // This default implementation calls the Write(Object, int, int)
        // method to write the character array.
        //
        public virtual void Write(char[] chars)
        {
            ArgumentNullException.ThrowIfNull(chars);
 
            WriteCharsCommonWithoutLengthPrefix(chars, useThisWriteOverride: false);
        }
 
        // Writes a section of a character array to this stream.
        //
        // This default implementation calls the Write(Object, int, int)
        // method to write the character array.
        //
        public virtual void Write(char[] chars, int index, int count)
        {
            ArgumentNullException.ThrowIfNull(chars);
 
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            if (index > chars.Length - count)
                throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_IndexCount);
 
            WriteCharsCommonWithoutLengthPrefix(chars.AsSpan(index, count), useThisWriteOverride: false);
        }
 
        // Writes a double to this stream. The current position of the stream is
        // advanced by eight.
        //
        public virtual void Write(double value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(double)];
            BinaryPrimitives.WriteDoubleLittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        public virtual void Write(decimal value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(decimal)];
            decimal.GetBytes(value, buffer);
            OutStream.Write(buffer);
        }
 
        // Writes a two-byte signed integer to this stream. The current position of
        // the stream is advanced by two.
        //
        public virtual void Write(short value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(short)];
            BinaryPrimitives.WriteInt16LittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes a two-byte unsigned integer to this stream. The current position
        // of the stream is advanced by two.
        //
        [CLSCompliant(false)]
        public virtual void Write(ushort value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(ushort)];
            BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes a four-byte signed integer to this stream. The current position
        // of the stream is advanced by four.
        //
        public virtual void Write(int value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(int)];
            BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes a four-byte unsigned integer to this stream. The current position
        // of the stream is advanced by four.
        //
        [CLSCompliant(false)]
        public virtual void Write(uint value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(uint)];
            BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes an eight-byte signed integer to this stream. The current position
        // of the stream is advanced by eight.
        //
        public virtual void Write(long value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(long)];
            BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes an eight-byte unsigned integer to this stream. The current
        // position of the stream is advanced by eight.
        //
        [CLSCompliant(false)]
        public virtual void Write(ulong value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(ulong)];
            BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes a float to this stream. The current position of the stream is
        // advanced by four.
        //
        public virtual void Write(float value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(float)];
            BinaryPrimitives.WriteSingleLittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes a half to this stream. The current position of the stream is
        // advanced by two.
        //
        public virtual void Write(Half value)
        {
            Span<byte> buffer = stackalloc byte[sizeof(ushort) /* = sizeof(Half) */];
            BinaryPrimitives.WriteHalfLittleEndian(buffer, value);
            OutStream.Write(buffer);
        }
 
        // Writes a length-prefixed string to this stream in the BinaryWriter's
        // current Encoding. This method first writes the length of the string as
        // an encoded unsigned integer with variable length, and then writes that many characters
        // to the stream.
        //
        public virtual void Write(string value)
        {
            ArgumentNullException.ThrowIfNull(value);
 
            // Common: UTF-8, small string, avoid 2-pass calculation
            // Less common: UTF-8, large string, avoid 2-pass calculation
            // Uncommon: excessively large string or not UTF-8
 
            if (_useFastUtf8)
            {
                if (value.Length <= 127 / 3)
                {
                    // Max expansion: each char -> 3 bytes, so 127 bytes max of data, +1 for length prefix
                    Span<byte> buffer = stackalloc byte[128];
                    int actualByteCount = _encoding.GetBytes(value, buffer.Slice(1));
                    buffer[0] = (byte)actualByteCount; // bypass call to Write7BitEncodedInt
                    OutStream.Write(buffer.Slice(0, actualByteCount + 1 /* length prefix */));
                    return;
                }
                else if (value.Length <= MaxArrayPoolRentalSize / 3)
                {
                    byte[] rented = ArrayPool<byte>.Shared.Rent(value.Length * 3); // max expansion: each char -> 3 bytes
                    int actualByteCount = _encoding.GetBytes(value, rented);
                    Write7BitEncodedInt(actualByteCount);
                    OutStream.Write(rented, 0, actualByteCount);
                    ArrayPool<byte>.Shared.Return(rented);
                    return;
                }
            }
 
            // Slow path: not fast UTF-8, or data is very large. We need to fall back
            // to a 2-pass mechanism so that we're not renting absurdly large arrays.
 
            int actualBytecount = _encoding.GetByteCount(value);
            Write7BitEncodedInt(actualBytecount);
            WriteCharsCommonWithoutLengthPrefix(value, useThisWriteOverride: false);
        }
 
        public virtual void Write(ReadOnlySpan<byte> buffer)
        {
            if (GetType() == typeof(BinaryWriter))
            {
                OutStream.Write(buffer);
            }
            else
            {
                byte[] array = ArrayPool<byte>.Shared.Rent(buffer.Length);
                try
                {
                    buffer.CopyTo(array);
                    Write(array, 0, buffer.Length);
                }
                finally
                {
                    ArrayPool<byte>.Shared.Return(array);
                }
            }
        }
 
        public virtual void Write(ReadOnlySpan<char> chars)
        {
            // When Write(ROS<char>) was first introduced, it dispatched to the this.Write(byte[], ...)
            // virtual method rather than write directly to the output stream. We maintain that same
            // double-indirection for compat purposes.
            WriteCharsCommonWithoutLengthPrefix(chars, useThisWriteOverride: true);
        }
 
        private void WriteCharsCommonWithoutLengthPrefix(ReadOnlySpan<char> chars, bool useThisWriteOverride)
        {
            // If our input is truly enormous, the call to GetMaxByteCount might overflow,
            // which we want to avoid. Theoretically, any Encoding could expand from chars -> bytes
            // at an enormous ratio and cause us problems anyway given small inputs, but this is so
            // unrealistic that we needn't worry about it.
 
            byte[] rented;
 
            if (chars.Length <= MaxArrayPoolRentalSize)
            {
                // GetByteCount may walk the buffer contents, resulting in 2 passes over the data.
                // We prefer GetMaxByteCount because it's a constant-time operation.
 
                int maxByteCount = _encoding.GetMaxByteCount(chars.Length);
                if (maxByteCount <= MaxArrayPoolRentalSize)
                {
                    rented = ArrayPool<byte>.Shared.Rent(maxByteCount);
                    int actualByteCount = _encoding.GetBytes(chars, rented);
                    WriteToOutStream(rented, 0, actualByteCount, useThisWriteOverride);
                    ArrayPool<byte>.Shared.Return(rented);
                    return;
                }
            }
 
            // We're dealing with an enormous amount of data, so acquire an Encoder.
            // It should be rare that callers pass sufficiently large inputs to hit
            // this code path, and the cost of the operation is dominated by the transcoding
            // step anyway, so it's ok for us to take the allocation here.
 
            rented = ArrayPool<byte>.Shared.Rent(MaxArrayPoolRentalSize);
            Encoder encoder = _encoding.GetEncoder();
            bool completed;
 
            do
            {
                encoder.Convert(chars, rented, flush: true, out int charsConsumed, out int bytesWritten, out completed);
                if (bytesWritten != 0)
                {
                    WriteToOutStream(rented, 0, bytesWritten, useThisWriteOverride);
                }
 
                chars = chars.Slice(charsConsumed);
            } while (!completed);
 
            ArrayPool<byte>.Shared.Return(rented);
 
            void WriteToOutStream(byte[] buffer, int offset, int count, bool useThisWriteOverride)
            {
                if (useThisWriteOverride)
                {
                    Write(buffer, offset, count); // bounce through this.Write(...) overridden logic
                }
                else
                {
                    OutStream.Write(buffer, offset, count); // ignore this.Write(...) override, go straight to inner stream
                }
            }
        }
 
        public void Write7BitEncodedInt(int value)
        {
            uint uValue = (uint)value;
 
            // Write out an int 7 bits at a time. The high bit of the byte,
            // when on, tells reader to continue reading more bytes.
            //
            // Using the constants 0x7F and ~0x7F below offers smaller
            // codegen than using the constant 0x80.
 
            while (uValue > 0x7Fu)
            {
                Write((byte)(uValue | ~0x7Fu));
                uValue >>= 7;
            }
 
            Write((byte)uValue);
        }
 
        public void Write7BitEncodedInt64(long value)
        {
            ulong uValue = (ulong)value;
 
            // Write out an int 7 bits at a time. The high bit of the byte,
            // when on, tells reader to continue reading more bytes.
            //
            // Using the constants 0x7F and ~0x7F below offers smaller
            // codegen than using the constant 0x80.
 
            while (uValue > 0x7Fu)
            {
                Write((byte)((uint)uValue | ~0x7Fu));
                uValue >>= 7;
            }
 
            Write((byte)uValue);
        }
    }
}