diff --git a/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 04dd510bb1c79f..866a53cd247bc4 100644 --- a/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -14,6 +14,8 @@ + + @@ -21,6 +23,8 @@ + + diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs index e78546ad852f2e..360df721c085db 100644 --- a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.cs @@ -13,7 +13,10 @@ namespace System.Net.Http.Json { - internal sealed class TranscodingReadStream : Stream + /// + /// Adds a transcode-to-UTF-8 layer to the read operations on another stream. + /// + internal sealed partial class TranscodingReadStream : Stream { private static readonly int OverflowBufferSize = Encoding.UTF8.GetMaxByteCount(1); // The most number of bytes used to represent a single UTF char @@ -25,9 +28,10 @@ internal sealed class TranscodingReadStream : Stream private readonly Decoder _decoder; private readonly Encoder _encoder; - private ArraySegment _byteBuffer; - private ArraySegment _charBuffer; - private ArraySegment _overflowBuffer; + private byte[] _pooledBytes; + private byte[] _pooledOverflowBytes; + private char[] _pooledChars; + private bool _disposed; public TranscodingReadStream(Stream input, Encoding sourceEncoding) @@ -36,15 +40,17 @@ public TranscodingReadStream(Stream input, Encoding sourceEncoding) // The "count" in the buffer is the size of any content from a previous read. // Initialize them to 0 since nothing has been read so far. - _byteBuffer = new ArraySegment(ArrayPool.Shared.Rent(MaxByteBufferSize), 0, count: 0); + _pooledBytes = ArrayPool.Shared.Rent(MaxByteBufferSize); // Attempt to allocate a char buffer than can tolerate the worst-case scenario for this // encoding. This would allow the byte -> char conversion to complete in a single call. // The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once. int maxCharBufferSize = sourceEncoding.GetMaxCharCount(MaxByteBufferSize); - _charBuffer = new ArraySegment(ArrayPool.Shared.Rent(maxCharBufferSize), 0, count: 0); + _pooledChars = ArrayPool.Shared.Rent(maxCharBufferSize); + + _pooledOverflowBytes = ArrayPool.Shared.Rent(OverflowBufferSize); - _overflowBuffer = new ArraySegment(ArrayPool.Shared.Rent(OverflowBufferSize), 0, count: 0); + InitializeBuffers(); _decoder = sourceEncoding.GetDecoder(); _encoder = Encoding.UTF8.GetEncoder(); @@ -61,136 +67,9 @@ public override long Position set => throw new NotSupportedException(); } - internal int ByteBufferCount => _byteBuffer.Count; - internal int CharBufferCount => _charBuffer.Count; - internal int OverflowCount => _overflowBuffer.Count; - public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (buffer.Length - offset < count) - { - throw new ArgumentException(SR.Argument_InvalidOffLen); - } - - var readBuffer = new ArraySegment(buffer, offset, count); - return ReadAsyncCore(readBuffer, cancellationToken); - } - - private async Task ReadAsyncCore(ArraySegment readBuffer, CancellationToken cancellationToken) - { - if (readBuffer.Count == 0) - { - return 0; - } - - if (_overflowBuffer.Count > 0) - { - int bytesToCopy = Math.Min(readBuffer.Count, _overflowBuffer.Count); - _overflowBuffer.Slice(0, bytesToCopy).CopyTo(readBuffer); - - _overflowBuffer = _overflowBuffer.Slice(bytesToCopy); - - // If we have any overflow bytes, avoid complicating the remainder of the code, by returning as - // soon as we copy any content. - return bytesToCopy; - } - - bool shouldFlushEncoder = false; - // Only read more content from the input stream if we have exhausted all the buffered chars. - if (_charBuffer.Count == 0) - { - int bytesRead = await ReadInputChars(cancellationToken).ConfigureAwait(false); - shouldFlushEncoder = bytesRead == 0 && _byteBuffer.Count == 0; - } - - bool completed = false; - int charsRead = default; - int bytesWritten = default; - // Since Convert() could fail if the destination buffer cannot fit at least one encoded char. - // If the destination buffer is smaller than GetMaxByteCount(1), we avoid encoding to the destination and we use the overflow buffer instead. - if (readBuffer.Count > OverflowBufferSize || _charBuffer.Count == 0) - { - _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, readBuffer.Array!, readBuffer.Offset, readBuffer.Count, - flush: shouldFlushEncoder, out charsRead, out bytesWritten, out completed); - } - - _charBuffer = _charBuffer.Slice(charsRead); - - if (completed || bytesWritten > 0) - { - return bytesWritten; - } - - _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, byteIndex: 0, _overflowBuffer.Array!.Length, - flush: shouldFlushEncoder, out int overFlowChars, out int overflowBytes, out completed); - - Debug.Assert(overflowBytes > 0 && overFlowChars > 0, "We expect writes to the overflow buffer to always succeed since it is large enough to accommodate at least one char."); - - _charBuffer = _charBuffer.Slice(overFlowChars); - - // readBuffer: [ 0, 0, ], overflowBuffer: [ 7, 13, 34, ] - // Fill up the readBuffer to capacity, so the result looks like so: - // readBuffer: [ 7, 13 ], overflowBuffer: [ 34 ] - Debug.Assert(readBuffer.Count < overflowBytes); - _overflowBuffer.Array.AsSpan(0, readBuffer.Count).CopyTo(readBuffer); - - Debug.Assert(_overflowBuffer.Array != null); - - _overflowBuffer = new ArraySegment(_overflowBuffer.Array, readBuffer.Count, overflowBytes - readBuffer.Count); - - Debug.Assert(_overflowBuffer.Count > 0); - - return readBuffer.Count; - } - - private async Task ReadInputChars(CancellationToken cancellationToken) - { - // If we had left-over bytes from a previous read, move it to the start of the buffer and read content into - // the segment that follows. - Debug.Assert(_byteBuffer.Array != null); - Buffer.BlockCopy(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Array, 0, _byteBuffer.Count); - - int offset = _byteBuffer.Count; - int count = _byteBuffer.Array.Length - _byteBuffer.Count; - - int bytesRead = await _stream.ReadAsync(_byteBuffer.Array, offset, count, cancellationToken).ConfigureAwait(false); - - _byteBuffer = new ArraySegment(_byteBuffer.Array, 0, offset + bytesRead); - - Debug.Assert(_byteBuffer.Array != null); - Debug.Assert(_charBuffer.Array != null); - Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read"); - - _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, charIndex: 0, _charBuffer.Array.Length, - flush: bytesRead == 0, out int bytesUsed, out int charsUsed, out _); - - // We flush only when the stream is exhausted and there are no pending bytes in the buffer. - Debug.Assert(bytesRead != 0 || _byteBuffer.Count - bytesUsed == 0); - - _byteBuffer = _byteBuffer.Slice(bytesUsed); - _charBuffer = new ArraySegment(_charBuffer.Array, 0, charsUsed); - - return bytesRead; - } - public override void Flush() => throw new NotSupportedException(); @@ -209,17 +88,17 @@ protected override void Dispose(bool disposing) { _disposed = true; - Debug.Assert(_charBuffer.Array != null); - ArrayPool.Shared.Return(_charBuffer.Array); - _charBuffer = default; + Debug.Assert(_pooledChars != null); + ArrayPool.Shared.Return(_pooledChars); + _pooledChars = null!; - Debug.Assert(_byteBuffer.Array != null); - ArrayPool.Shared.Return(_byteBuffer.Array); - _byteBuffer = default; + Debug.Assert(_pooledBytes != null); + ArrayPool.Shared.Return(_pooledBytes); + _pooledBytes = null!; - Debug.Assert(_overflowBuffer.Array != null); - ArrayPool.Shared.Return(_overflowBuffer.Array); - _overflowBuffer = default; + Debug.Assert(_pooledOverflowBytes != null); + ArrayPool.Shared.Return(_pooledOverflowBytes); + _pooledOverflowBytes = null!; _stream.Dispose(); } diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.netcoreapp.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.netcoreapp.cs new file mode 100644 index 00000000000000..7d73960aedb76f --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.netcoreapp.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed partial class TranscodingReadStream : Stream + { + private Memory _byteBuffer; + private Memory _charBuffer; + private Memory _overflowBuffer; + + internal int ByteBufferCount => _byteBuffer.Length; + internal int CharBufferCount => _charBuffer.Length; + internal int OverflowCount => _overflowBuffer.Length; + + private void InitializeBuffers() { } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public async override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (buffer.IsEmpty) + { + return 0; + } + + if (!_overflowBuffer.IsEmpty) + { + int bytesToCopy = Math.Min(buffer.Length, _overflowBuffer.Length); + + _overflowBuffer.Slice(0, bytesToCopy).CopyTo(buffer); + _overflowBuffer = _overflowBuffer.Slice(bytesToCopy); + + // If we have any overflow bytes, avoid complicating the remainder of the code, by returning as + // soon as we copy any content. + return bytesToCopy; + } + + bool shouldFlushEncoder = false; + // Only read more content from the input stream if we have exhausted all the buffered chars. + if (_charBuffer.IsEmpty) + { + int bytesRead = await ReadInputChars(cancellationToken).ConfigureAwait(false); + shouldFlushEncoder = bytesRead == 0 && _byteBuffer.Length == 0; + } + + bool completed = false; + int charsRead = default; + int bytesWritten = default; + // Since Convert() could fail if the destination buffer cannot fit at least one encoded char. + // If the destination buffer is smaller than GetMaxByteCount(1), we avoid encoding to the destination and we use the overflow buffer instead. + if (buffer.Length > OverflowBufferSize || _charBuffer.IsEmpty) + { + _encoder.Convert(_charBuffer.Span, buffer.Span, flush: shouldFlushEncoder, out charsRead, out bytesWritten, out completed); + } + + _charBuffer = _charBuffer.Slice(charsRead); + + if (completed || bytesWritten > 0) + { + return bytesWritten; + } + + // If the buffer was too small, transcode to the overflow buffer. + _overflowBuffer = new Memory(_pooledOverflowBytes); + _encoder.Convert(_charBuffer.Span, _overflowBuffer.Span, flush: shouldFlushEncoder, out charsRead, out bytesWritten, out _); + Debug.Assert(bytesWritten > 0 && charsRead > 0, "We expect writes to the overflow buffer to always succeed since it is large enough to accommodate at least one char."); + + _charBuffer = _charBuffer.Slice(charsRead); + _overflowBuffer = _overflowBuffer.Slice(0, bytesWritten); + + Debug.Assert(buffer.Length < bytesWritten); + _overflowBuffer.Slice(0, buffer.Length).CopyTo(buffer); + + _overflowBuffer = _overflowBuffer.Slice(buffer.Length); + + return buffer.Length; + } + + private async ValueTask ReadInputChars(CancellationToken cancellationToken) + { + // If we had left-over bytes from a previous read, move it to the start of the buffer and read content into + // the space that follows. + ReadOnlyMemory previousBytes = _byteBuffer; + _byteBuffer = new Memory(_pooledBytes); + previousBytes.CopyTo(_byteBuffer); + + int bytesRead = await _stream.ReadAsync(_byteBuffer.Slice(previousBytes.Length), cancellationToken).ConfigureAwait(false); + + Debug.Assert(_charBuffer.IsEmpty, "We should only expect to read more input chars once all buffered content is read"); + + _charBuffer = new Memory(_pooledChars); + _byteBuffer = _byteBuffer.Slice(0, previousBytes.Length + bytesRead); + + _decoder.Convert(_byteBuffer.Span, _charBuffer.Span, flush: bytesRead == 0, out int bytesUsed, out int charsUsed, out _); + + // We flush only when the stream is exhausted and there are no pending bytes in the buffer. + Debug.Assert(bytesRead != 0 || _byteBuffer.Length - bytesUsed == 0); + + _byteBuffer = _byteBuffer.Slice(bytesUsed); + _charBuffer = _charBuffer.Slice(0, charsUsed); + + return bytesRead; + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.netstandard.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.netstandard.cs new file mode 100644 index 00000000000000..3960dbdc015706 --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingReadStream.netstandard.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed partial class TranscodingReadStream : Stream + { + private ArraySegment _byteBuffer; + private ArraySegment _charBuffer; + private ArraySegment _overflowBuffer; + + internal int ByteBufferCount => _byteBuffer.Count; + internal int CharBufferCount => _charBuffer.Count; + internal int OverflowCount => _overflowBuffer.Count; + + private void InitializeBuffers() + { + _byteBuffer = new ArraySegment(_pooledBytes, 0, count: 0); + _charBuffer = new ArraySegment(_pooledChars, 0, count: 0); + _overflowBuffer = new ArraySegment(_pooledOverflowBytes, 0, count: 0); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + var readBuffer = new ArraySegment(buffer, offset, count); + return ReadAsyncCore(readBuffer, cancellationToken); + } + + private async Task ReadAsyncCore(ArraySegment readBuffer, CancellationToken cancellationToken) + { + if (readBuffer.Count == 0) + { + return 0; + } + + if (_overflowBuffer.Count > 0) + { + int bytesToCopy = Math.Min(readBuffer.Count, _overflowBuffer.Count); + _overflowBuffer.Slice(0, bytesToCopy).CopyTo(readBuffer); + + _overflowBuffer = _overflowBuffer.Slice(bytesToCopy); + + // If we have any overflow bytes, avoid complicating the remainder of the code, by returning as + // soon as we copy any content. + return bytesToCopy; + } + + bool shouldFlushEncoder = false; + // Only read more content from the input stream if we have exhausted all the buffered chars. + if (_charBuffer.Count == 0) + { + int bytesRead = await ReadInputChars(cancellationToken).ConfigureAwait(false); + shouldFlushEncoder = bytesRead == 0 && _byteBuffer.Count == 0; + } + + bool completed = false; + int charsRead = default; + int bytesWritten = default; + // Since Convert() could fail if the destination buffer cannot fit at least one encoded char. + // If the destination buffer is smaller than GetMaxByteCount(1), we avoid encoding to the destination and we use the overflow buffer instead. + if (readBuffer.Count > OverflowBufferSize || _charBuffer.Count == 0) + { + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, readBuffer.Array!, readBuffer.Offset, readBuffer.Count, + flush: shouldFlushEncoder, out charsRead, out bytesWritten, out completed); + } + + _charBuffer = _charBuffer.Slice(charsRead); + + if (completed || bytesWritten > 0) + { + return bytesWritten; + } + + _encoder.Convert(_charBuffer.Array!, _charBuffer.Offset, _charBuffer.Count, _overflowBuffer.Array!, byteIndex: 0, _overflowBuffer.Array!.Length, + flush: shouldFlushEncoder, out int overFlowChars, out int overflowBytes, out completed); + + Debug.Assert(overflowBytes > 0 && overFlowChars > 0, "We expect writes to the overflow buffer to always succeed since it is large enough to accommodate at least one char."); + + _charBuffer = _charBuffer.Slice(overFlowChars); + + // readBuffer: [ 0, 0, ], overflowBuffer: [ 7, 13, 34, ] + // Fill up the readBuffer to capacity, so the result looks like so: + // readBuffer: [ 7, 13 ], overflowBuffer: [ 34 ] + Debug.Assert(readBuffer.Count < overflowBytes); + _overflowBuffer.Array.AsSpan(0, readBuffer.Count).CopyTo(readBuffer); + + Debug.Assert(_overflowBuffer.Array != null); + + _overflowBuffer = new ArraySegment(_overflowBuffer.Array, readBuffer.Count, overflowBytes - readBuffer.Count); + + Debug.Assert(_overflowBuffer.Count > 0); + + return readBuffer.Count; + } + + private async ValueTask ReadInputChars(CancellationToken cancellationToken) + { + // If we had left-over bytes from a previous read, move it to the start of the buffer and read content into + // the segment that follows. + Debug.Assert(_byteBuffer.Array != null); + Buffer.BlockCopy(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Array, 0, _byteBuffer.Count); + + int offset = _byteBuffer.Count; + int count = _byteBuffer.Array.Length - _byteBuffer.Count; + + int bytesRead = await _stream.ReadAsync(_byteBuffer.Array, offset, count, cancellationToken).ConfigureAwait(false); + + _byteBuffer = new ArraySegment(_byteBuffer.Array, 0, offset + bytesRead); + + Debug.Assert(_byteBuffer.Array != null); + Debug.Assert(_charBuffer.Array != null); + Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read"); + + _decoder.Convert(_byteBuffer.Array, _byteBuffer.Offset, _byteBuffer.Count, _charBuffer.Array, charIndex: 0, _charBuffer.Array.Length, + flush: bytesRead == 0, out int bytesUsed, out int charsUsed, out _); + + // We flush only when the stream is exhausted and there are no pending bytes in the buffer. + Debug.Assert(bytesRead != 0 || _byteBuffer.Count - bytesUsed == 0); + + _byteBuffer = _byteBuffer.Slice(bytesUsed); + _charBuffer = new ArraySegment(_charBuffer.Array, 0, charsUsed); + + return bytesRead; + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs index 1135ad4bc1142b..851d7f3dc9c6f2 100644 --- a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.cs @@ -12,20 +12,22 @@ namespace System.Net.Http.Json { - internal sealed class TranscodingWriteStream : Stream + /// + /// Adds a transcode-from-UTF-8 layer to the write operations on another stream. + /// + internal sealed partial class TranscodingWriteStream : Stream { // Default size of the char buffer that will hold the passed-in bytes when decoded from UTF-8. // The buffer holds them and then they are encoded to the targetEncoding and written to the underlying stream. internal const int MaxCharBufferSize = 4096; // Upper bound that limits the byte buffer size to prevent an encoding that has a very poor worst-case scenario. internal const int MaxByteBufferSize = 4 * MaxCharBufferSize; - private readonly int _maxByteBufferSize; private readonly Stream _stream; private readonly Decoder _decoder; private readonly Encoder _encoder; + private byte[] _byteBuffer; private char[] _charBuffer; - private int _charsDecoded; private bool _disposed; public TranscodingWriteStream(Stream stream, Encoding targetEncoding) @@ -37,7 +39,8 @@ public TranscodingWriteStream(Stream stream, Encoding targetEncoding) // Attempt to allocate a byte buffer than can tolerate the worst-case scenario for this // encoding. This would allow the char -> byte conversion to complete in a single call. // However limit the buffer size to prevent an encoding that has a very poor worst-case scenario. - _maxByteBufferSize = Math.Min(MaxByteBufferSize, targetEncoding.GetMaxByteCount(MaxCharBufferSize)); + int maxByteBufferSize = Math.Min(MaxByteBufferSize, targetEncoding.GetMaxByteCount(MaxCharBufferSize)); + _byteBuffer = ArrayPool.Shared.Rent(maxByteBufferSize); _decoder = Encoding.UTF8.GetDecoder(); _encoder = targetEncoding.GetEncoder(); @@ -50,7 +53,7 @@ public TranscodingWriteStream(Stream stream, Encoding targetEncoding) public override long Position { get; set; } public override void Flush() - => throw new NotSupportedException(); + => _stream.Flush(); public override Task FlushAsync(CancellationToken cancellationToken) => _stream.FlushAsync(cancellationToken); @@ -64,96 +67,44 @@ public override long Seek(long offset, SeekOrigin origin) public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) - => throw new NotSupportedException(); - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + protected override void Dispose(bool disposing) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (buffer.Length - offset < count) + if (!_disposed) { - throw new ArgumentException(SR.Argument_InvalidOffLen); - } - - var bufferSegment = new ArraySegment(buffer, offset, count); - return WriteAsyncCore(bufferSegment, cancellationToken); - } - - private async Task WriteAsyncCore(ArraySegment bufferSegment, CancellationToken cancellationToken) - { - bool decoderCompleted = false; + _disposed = true; - while (!decoderCompleted) - { - _decoder.Convert(bufferSegment.Array!, bufferSegment.Offset, bufferSegment.Count, _charBuffer, _charsDecoded, _charBuffer.Length - _charsDecoded, - flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); + ArrayPool.Shared.Return(_charBuffer); + _charBuffer = null!; - _charsDecoded += charsDecoded; - bufferSegment = bufferSegment.Slice(bytesDecoded); - await WriteBufferAsync(cancellationToken).ConfigureAwait(false); + ArrayPool.Shared.Return(_byteBuffer); + _byteBuffer = null!; } } - private async Task WriteBufferAsync(CancellationToken cancellationToken) + public async ValueTask FinalWriteAsync(CancellationToken cancellationToken) { + // Flush the encoder. bool encoderCompleted = false; - int charsWritten = 0; - byte[] byteBuffer = ArrayPool.Shared.Rent(_maxByteBufferSize); - - while (!encoderCompleted && charsWritten < _charsDecoded) + while (!encoderCompleted) { - _encoder.Convert(_charBuffer, charsWritten, _charsDecoded - charsWritten, byteBuffer, byteIndex: 0, byteBuffer.Length, - flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); - - await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); - charsWritten += charsEncoded; - } - - ArrayPool.Shared.Return(byteBuffer); - - // At this point, we've written all the buffered chars to the underlying Stream. - _charsDecoded = 0; - } + _encoder.Convert(Array.Empty(), 0, 0, _byteBuffer, 0, _byteBuffer.Length, + flush: true, out _, out int bytesUsed, out encoderCompleted); - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - _disposed = true; - ArrayPool.Shared.Return(_charBuffer); - _charBuffer = null!; + await _stream.WriteAsync(_byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); } } - public async Task FinalWriteAsync(CancellationToken cancellationToken) + public void FinalWrite() { // Flush the encoder. - byte[] byteBuffer = ArrayPool.Shared.Rent(_maxByteBufferSize); bool encoderCompleted = false; - while (!encoderCompleted) { - _encoder.Convert(Array.Empty(), 0, 0, byteBuffer, 0, byteBuffer.Length, + _encoder.Convert(Array.Empty(), 0, 0, _byteBuffer, 0, _byteBuffer.Length, flush: true, out _, out int bytesUsed, out encoderCompleted); - await _stream.WriteAsync(byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); + _stream.Write(_byteBuffer, 0, bytesUsed); } - - ArrayPool.Shared.Return(byteBuffer); } } } diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.netcoreapp.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.netcoreapp.cs new file mode 100644 index 00000000000000..c8a7c9f0129afc --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.netcoreapp.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed partial class TranscodingWriteStream : Stream + { + public override void Write(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + Write(new ReadOnlySpan(buffer, offset, count)); + } + + public override void Write(ReadOnlySpan buffer) + { + Span charBuffer = _charBuffer; + ReadOnlySpan bufferCopy = buffer; + + bool decoderCompleted = false; + while (!decoderCompleted) + { + _decoder.Convert(bufferCopy, charBuffer, + flush: false, out int bytesDecoded, out int charCount, out decoderCompleted); + + bufferCopy = bufferCopy.Slice(bytesDecoded); + + ReadOnlySpan encoderCharBuffer = charBuffer.Slice(0, charCount); + Span encoderByteBuffer = _byteBuffer; + + bool encoderCompleted = false; + while (!encoderCompleted && encoderCharBuffer.Length > 0) + { + _encoder.Convert(encoderCharBuffer, encoderByteBuffer, + flush: false, out int charsUsed, out int bytesUsed, out encoderCompleted); + + _stream.Write(encoderByteBuffer.Slice(0, bytesUsed)); + encoderCharBuffer = encoderCharBuffer.Slice(charsUsed); + } + } + + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + return WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + Memory charBuffer = _charBuffer; + ReadOnlyMemory bufferCopy = buffer; + + bool decoderCompleted = false; + while (!decoderCompleted) + { + _decoder.Convert(bufferCopy.Span, charBuffer.Span, + flush: false, out int bytesDecoded, out int charCount, out decoderCompleted); + + bufferCopy = bufferCopy.Slice(bytesDecoded); + + ReadOnlyMemory encoderCharBuffer = charBuffer.Slice(0, charCount); + Memory encoderByteBuffer = _byteBuffer; + + bool encoderCompleted = false; + while (!encoderCompleted && encoderCharBuffer.Length > 0) + { + _encoder.Convert(encoderCharBuffer.Span, encoderByteBuffer.Span, + flush: false, out int charsUsed, out int bytesUsed, out encoderCompleted); + + await _stream.WriteAsync(encoderByteBuffer.Slice(0, bytesUsed), cancellationToken).ConfigureAwait(false); + encoderCharBuffer = encoderCharBuffer.Slice(charsUsed); + } + } + } + } +} diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.netstandard.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.netstandard.cs new file mode 100644 index 00000000000000..7d85b9d54ad08f --- /dev/null +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/TranscodingWriteStream.netstandard.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Json +{ + internal sealed partial class TranscodingWriteStream : Stream + { + public override void Write(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + var bufferSegment = new ArraySegment(buffer, offset, count); + + int charCount = 0; + bool decoderCompleted = false; + while (!decoderCompleted) + { + _decoder.Convert(bufferSegment.Array!, bufferSegment.Offset, bufferSegment.Count, _charBuffer, charCount, _charBuffer.Length - charCount, + flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); + + charCount += charsDecoded; + bufferSegment = bufferSegment.Slice(bytesDecoded); + + int charsWritten = 0; + bool encoderCompleted = false; + while (!encoderCompleted && charsWritten < charCount) + { + _encoder.Convert(_charBuffer, charsWritten, charCount - charsWritten, _byteBuffer, 0, _byteBuffer.Length, + flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); + + _stream.Write(_byteBuffer, 0, bytesUsed); + charsWritten += charsEncoded; + } + + // At this point, we've written all the buffered chars to the underlying Stream. + charCount = 0; + } + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + var bufferSegment = new ArraySegment(buffer, offset, count); + return WriteAsyncCore(bufferSegment, cancellationToken); + } + + private async Task WriteAsyncCore(ArraySegment bufferSegment, CancellationToken cancellationToken) + { + int charCount = 0; + bool decoderCompleted = false; + while (!decoderCompleted) + { + _decoder.Convert(bufferSegment.Array!, bufferSegment.Offset, bufferSegment.Count, _charBuffer, charCount, _charBuffer.Length - charCount, + flush: false, out int bytesDecoded, out int charsDecoded, out decoderCompleted); + + charCount += charsDecoded; + bufferSegment = bufferSegment.Slice(bytesDecoded); + + int charsWritten = 0; + bool encoderCompleted = false; + while (!encoderCompleted && charsWritten < charCount) + { + _encoder.Convert(_charBuffer, charsWritten, charCount - charsWritten, _byteBuffer, 0, _byteBuffer.Length, + flush: false, out int charsEncoded, out int bytesUsed, out encoderCompleted); + + await _stream.WriteAsync(_byteBuffer, 0, bytesUsed, cancellationToken).ConfigureAwait(false); + charsWritten += charsEncoded; + } + + // At this point, we've written all the buffered chars to the underlying Stream. + charCount = 0; + } + } + } +} diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj b/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj index 4b7b36a6be21f5..f01ecc7bafc2f4 100644 --- a/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/System.Net.Http.Json.Unit.Tests.csproj @@ -6,10 +6,24 @@ + + + ProductionCode\System\Net\Http\Json\TranscodingReadStream.netcoreapp.cs + + + ProductionCode\System\Net\Http\Json\TranscodingWriteStream.netcoreapp.cs + + ProductionCode\System\ArraySegmentExtensions.netstandard.cs + + ProductionCode\System\Net\Http\Json\TranscodingReadStream.netstandard.cs + + + ProductionCode\System\Net\Http\Json\TranscodingWriteStream.netstandard.cs + diff --git a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs index 546f666c7c1133..3a5f43ab54c9cd 100644 --- a/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/UnitTests/TranscodingWriteStreamTests.cs @@ -4,13 +4,10 @@ // Taken from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/test/Formatters/TranscodingWriteStreamTest.cs -using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -18,15 +15,60 @@ namespace System.Net.Http.Json.Functional.Tests { public class TranscodingWriteStreamTest { - public static TheoryData WriteAsyncInputLatin => + public static TheoryData WriteInputLatin => TranscodingReadStreamTest.GetLatinTextInput(TranscodingWriteStream.MaxCharBufferSize, TranscodingWriteStream.MaxByteBufferSize); - public static TheoryData WriteAsyncInputUnicode => + public static TheoryData WriteInputUnicode => TranscodingReadStreamTest.GetUnicodeText(TranscodingWriteStream.MaxCharBufferSize); [Theory] - [MemberData(nameof(WriteAsyncInputLatin))] - [MemberData(nameof(WriteAsyncInputUnicode))] + [MemberData(nameof(WriteInputLatin))] + [MemberData(nameof(WriteInputUnicode))] + public void Write_Works_WhenOutputIs_UTF32(string message) + { + Encoding targetEncoding = Encoding.UTF32; + WriteTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteInputLatin))] + [MemberData(nameof(WriteInputUnicode))] + public void Write_Works_WhenOutputIs_Unicode(string message) + { + Encoding targetEncoding = Encoding.Unicode; + WriteTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteInputLatin))] + public void Write_Works_WhenOutputIs_UTF7(string message) + { + Encoding targetEncoding = Encoding.UTF7; + WriteTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteInputLatin))] + public void Write_Works_WhenOutputIs_WesternEuropeanEncoding(string message) + { + // Arrange + Encoding targetEncoding = Encoding.GetEncoding(28591); + WriteTest(targetEncoding, message); + } + + + [Theory] + [MemberData(nameof(WriteInputLatin))] + public void Write_Works_WhenOutputIs_ASCII(string message) + { + // Arrange + Encoding targetEncoding = Encoding.ASCII; + WriteTest(targetEncoding, message); + } + + [Theory] + [MemberData(nameof(WriteInputLatin))] + [MemberData(nameof(WriteInputUnicode))] public Task WriteAsync_Works_WhenOutputIs_UTF32(string message) { Encoding targetEncoding = Encoding.UTF32; @@ -34,8 +76,8 @@ public Task WriteAsync_Works_WhenOutputIs_UTF32(string message) } [Theory] - [MemberData(nameof(WriteAsyncInputLatin))] - [MemberData(nameof(WriteAsyncInputUnicode))] + [MemberData(nameof(WriteInputLatin))] + [MemberData(nameof(WriteInputUnicode))] public Task WriteAsync_Works_WhenOutputIs_Unicode(string message) { Encoding targetEncoding = Encoding.Unicode; @@ -43,7 +85,7 @@ public Task WriteAsync_Works_WhenOutputIs_Unicode(string message) } [Theory] - [MemberData(nameof(WriteAsyncInputLatin))] + [MemberData(nameof(WriteInputLatin))] public Task WriteAsync_Works_WhenOutputIs_UTF7(string message) { Encoding targetEncoding = Encoding.UTF7; @@ -51,7 +93,7 @@ public Task WriteAsync_Works_WhenOutputIs_UTF7(string message) } [Theory] - [MemberData(nameof(WriteAsyncInputLatin))] + [MemberData(nameof(WriteInputLatin))] public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message) { // Arrange @@ -61,7 +103,7 @@ public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message [Theory] - [MemberData(nameof(WriteAsyncInputLatin))] + [MemberData(nameof(WriteInputLatin))] public Task WriteAsync_Works_WhenOutputIs_ASCII(string message) { // Arrange @@ -69,6 +111,20 @@ public Task WriteAsync_Works_WhenOutputIs_ASCII(string message) return WriteAsyncTest(targetEncoding, message); } + private static void WriteTest(Encoding targetEncoding, string expected) + { + byte[] encodedMessage = Encoding.UTF8.GetBytes(expected); + var stream = new MemoryStream(); + + var transcodingStream = new TranscodingWriteStream(stream, targetEncoding); + transcodingStream.Write(encodedMessage, 0, encodedMessage.Length); + transcodingStream.Flush(); + transcodingStream.FinalWrite(); + + string actual = targetEncoding.GetString(stream.ToArray()); + Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase); + } + private static async Task WriteAsyncTest(Encoding targetEncoding, string message) { string expected = $"{{\"Message\":\"{JavaScriptEncoder.Default.Encode(message)}\"}}";