// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.Framework.MemoryPool; namespace Microsoft.AspNet.Mvc { /// /// Writes to the using the supplied . /// It does not write the BOM and also does not close the stream. /// public class HttpResponseStreamWriter : TextWriter { /// /// Default buffer size. /// public const int DefaultBufferSize = 1024; private readonly Stream _stream; private Encoder _encoder; private LeasedArraySegment _leasedByteBuffer; private LeasedArraySegment _leasedCharBuffer; private ArraySegment _byteBuffer; private ArraySegment _charBuffer; private int _charBufferSize; private int _charBufferCount; public HttpResponseStreamWriter(Stream stream, Encoding encoding) : this(stream, encoding, DefaultBufferSize) { } public HttpResponseStreamWriter(Stream stream, Encoding encoding, int bufferSize) { if (stream == null) { throw new ArgumentNullException(nameof(stream)); } if (encoding == null) { throw new ArgumentNullException(nameof(encoding)); } _stream = stream; Encoding = encoding; _charBufferSize = bufferSize; _encoder = encoding.GetEncoder(); _byteBuffer = new ArraySegment(new byte[encoding.GetMaxByteCount(bufferSize)]); _charBuffer = new ArraySegment(new char[bufferSize]); } public HttpResponseStreamWriter( Stream stream, Encoding encoding, LeasedArraySegment leasedByteBuffer, LeasedArraySegment leasedCharBuffer) { if (stream == null) { throw new ArgumentNullException(nameof(stream)); } if (encoding == null) { throw new ArgumentNullException(nameof(encoding)); } if (leasedByteBuffer == null) { throw new ArgumentNullException(nameof(leasedByteBuffer)); } if (leasedCharBuffer == null) { throw new ArgumentNullException(nameof(leasedCharBuffer)); } _stream = stream; Encoding = encoding; _leasedByteBuffer = leasedByteBuffer; _leasedCharBuffer = leasedCharBuffer; _encoder = encoding.GetEncoder(); _byteBuffer = leasedByteBuffer.Data; _charBuffer = leasedCharBuffer.Data; // We need to compute the usable size of the char buffer based on the size of the byte buffer. // Encoder.GetBytes assumes that the entirety of the byte[] passed in can be used, and that's not the // case with ArraySegments. _charBufferSize = Math.Min( leasedCharBuffer.Data.Count, encoding.GetMaxCharCount(leasedByteBuffer.Data.Count)); } public override Encoding Encoding { get; } public override void Write(char value) { if (_charBufferCount == _charBufferSize) { FlushInternal(); } _charBuffer.Array[_charBuffer.Offset + _charBufferCount] = value; _charBufferCount++; } public override void Write(char[] values, int index, int count) { if (values == null) { return; } while (count > 0) { if (_charBufferCount == _charBufferSize) { FlushInternal(); } CopyToCharBuffer(values, ref index, ref count); } } public override void Write(string value) { if (value == null) { return; } var count = value.Length; var index = 0; while (count > 0) { if (_charBufferCount == _charBufferSize) { FlushInternal(); } CopyToCharBuffer(value, ref index, ref count); } } public override async Task WriteAsync(char value) { if (_charBufferCount == _charBufferSize) { await FlushInternalAsync(); } _charBuffer.Array[_charBuffer.Offset + _charBufferCount] = value; _charBufferCount++; } public override async Task WriteAsync(char[] values, int index, int count) { if (values == null) { return; } while (count > 0) { if (_charBufferCount == _charBufferSize) { await FlushInternalAsync(); } CopyToCharBuffer(values, ref index, ref count); } } public override async Task WriteAsync(string value) { if (value == null) { return; } var count = value.Length; var index = 0; while (count > 0) { if (_charBufferCount == _charBufferSize) { await FlushInternalAsync(); } CopyToCharBuffer(value, ref index, ref count); } } // We want to flush the stream when Flush/FlushAsync is explicitly // called by the user (example: from a Razor view). public override void Flush() { FlushInternal(true, true); } public override Task FlushAsync() { return FlushInternalAsync(flushStream: true, flushEncoder: true); } // Do not flush the stream on Dispose, as this will cause response to be // sent in chunked encoding in case of Helios. protected override void Dispose(bool disposing) { FlushInternal(flushStream: false, flushEncoder: true); if (_leasedByteBuffer != null) { _leasedByteBuffer.Owner.Return(_leasedByteBuffer); } if (_leasedCharBuffer != null) { _leasedCharBuffer.Owner.Return(_leasedCharBuffer); } } private void FlushInternal(bool flushStream = false, bool flushEncoder = false) { if (_charBufferCount == 0) { return; } var count = _encoder.GetBytes( _charBuffer.Array, _charBuffer.Offset, _charBufferCount, _byteBuffer.Array, _byteBuffer.Offset, flushEncoder); if (count > 0) { _stream.Write(_byteBuffer.Array, _byteBuffer.Offset, count); } _charBufferCount = 0; if (flushStream) { _stream.Flush(); } } private async Task FlushInternalAsync(bool flushStream = false, bool flushEncoder = false) { if (_charBufferCount == 0) { return; } var count = _encoder.GetBytes( _charBuffer.Array, _charBuffer.Offset, _charBufferCount, _byteBuffer.Array, _byteBuffer.Offset, flushEncoder); if (count > 0) { await _stream.WriteAsync(_byteBuffer.Array, _byteBuffer.Offset, count); } _charBufferCount = 0; if (flushStream) { await _stream.FlushAsync(); } } private void CopyToCharBuffer(string value, ref int index, ref int count) { var remaining = Math.Min(_charBufferSize - _charBufferCount, count); value.CopyTo( sourceIndex: index, destination: _charBuffer.Array, destinationIndex: _charBuffer.Offset + _charBufferCount, count: remaining); _charBufferCount += remaining; index += remaining; count -= remaining; } private void CopyToCharBuffer(char[] values, ref int index, ref int count) { var remaining = Math.Min(_charBufferSize - _charBufferCount, count); Buffer.BlockCopy( src: values, srcOffset: index * sizeof(char), dst: _charBuffer.Array, dstOffset: (_charBuffer.Offset + _charBufferCount) * sizeof(char), count: remaining * sizeof(char)); _charBufferCount += remaining; index += remaining; count -= remaining; } } }