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)}\"}}";