Use Encoding.CreateTranscodingStream (#21509)

* Use Encoding.CreateTranscodingStream

Fixes https://github.com/dotnet/aspnetcore/issues/21243
This commit is contained in:
Pranav K 2020-05-06 16:59:39 -07:00 committed by GitHub
parent 5fd4f87977
commit 51f18b5652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 198 additions and 830 deletions

View File

@ -7,7 +7,6 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters.Json;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Formatters
@ -67,7 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
var httpContext = context.HttpContext;
var inputStream = GetInputStream(httpContext, encoding);
var (inputStream, usesTranscodingStream) = GetInputStream(httpContext, encoding);
object model;
try
@ -98,9 +97,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
finally
{
if (inputStream is TranscodingReadStream transcoding)
if (usesTranscodingStream)
{
await transcoding.DisposeAsync();
await inputStream.DisposeAsync();
}
}
@ -119,14 +118,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
}
private Stream GetInputStream(HttpContext httpContext, Encoding encoding)
private (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding encoding)
{
if (encoding.CodePage == Encoding.UTF8.CodePage)
{
return httpContext.Request.Body;
return (httpContext.Request.Body, false);
}
return new TranscodingReadStream(httpContext.Request.Body, encoding);
var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true);
return (inputStream, true);
}
private static class Log

View File

@ -2,14 +2,11 @@
// 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.Runtime.ExceptionServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters.Json;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
@ -73,44 +70,50 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var httpContext = context.HttpContext;
var writeStream = GetWriteStream(httpContext, selectedEncoding);
try
{
// context.ObjectType reflects the declared model type when specified.
// For polymorphic scenarios where the user declares a return type, but returns a derived type,
// we want to serialize all the properties on the derived type. This keeps parity with
// the behavior you get when the user does not declare the return type and with Json.Net at least at the top level.
var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);
await JsonSerializer.SerializeAsync(writeStream, context.Object, objectType, SerializerOptions);
// context.ObjectType reflects the declared model type when specified.
// For polymorphic scenarios where the user declares a return type, but returns a derived type,
// we want to serialize all the properties on the derived type. This keeps parity with
// the behavior you get when the user does not declare the return type and with Json.Net at least at the top level.
var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);
// The transcoding streams use Encoders and Decoders that have internal buffers. We need to flush these
// when there is no more data to be written. Stream.FlushAsync isn't suitable since it's
// acceptable to Flush a Stream (multiple times) prior to completion.
if (writeStream is TranscodingWriteStream transcodingStream)
{
await transcodingStream.FinalWriteAsync(CancellationToken.None);
}
await writeStream.FlushAsync();
}
finally
{
if (writeStream is TranscodingWriteStream transcodingStream)
{
await transcodingStream.DisposeAsync();
}
}
}
private Stream GetWriteStream(HttpContext httpContext, Encoding selectedEncoding)
{
var responseStream = httpContext.Response.Body;
if (selectedEncoding.CodePage == Encoding.UTF8.CodePage)
{
// JsonSerializer does not write a BOM. Therefore we do not have to handle it
// in any special way.
return httpContext.Response.Body;
await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions);
await responseStream.FlushAsync();
}
else
{
// JsonSerializer only emits UTF8 encoded output, but we need to write the response in the encoding specified by
// selectedEncoding
var transcodingStream = Encoding.CreateTranscodingStream(httpContext.Response.Body, selectedEncoding, Encoding.UTF8, leaveOpen: true);
return new TranscodingWriteStream(httpContext.Response.Body, selectedEncoding);
ExceptionDispatchInfo exceptionDispatchInfo = null;
try
{
await JsonSerializer.SerializeAsync(transcodingStream, context.Object, objectType, SerializerOptions);
await transcodingStream.FlushAsync();
}
catch (Exception ex)
{
// TranscodingStream may write to the inner stream as part of it's disposal.
// We do not want this exception "ex" to be eclipsed by any exception encountered during the write. We will stash it and
// explicitly rethrow it during the finally block.
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
}
finally
{
try
{
await transcodingStream.DisposeAsync();
}
catch when (exceptionDispatchInfo != null)
{
}
exceptionDispatchInfo?.Throw();
}
}
}
}
}

View File

@ -1,223 +0,0 @@
// 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.Buffers;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
{
internal sealed class TranscodingReadStream : Stream
{
private static readonly int OverflowBufferSize = Encoding.UTF8.GetMaxByteCount(1); // The most number of bytes used to represent a single UTF char
internal const int MaxByteBufferSize = 4096;
internal const int MaxCharBufferSize = 3 * MaxByteBufferSize;
private readonly Stream _stream;
private readonly Decoder _decoder;
private ArraySegment<byte> _byteBuffer;
private ArraySegment<char> _charBuffer;
private ArraySegment<byte> _overflowBuffer;
private bool _disposed;
public TranscodingReadStream(Stream input, Encoding sourceEncoding)
{
_stream = input;
// 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<byte>(
ArrayPool<byte>.Shared.Rent(MaxByteBufferSize),
0,
count: 0);
// 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.
// However limit the buffer size to prevent an encoding that has a very poor worst-case scenario.
// The conversion process is tolerant of char buffer that is not large enough to convert all the bytes at once.
var maxCharBufferSize = Math.Min(MaxCharBufferSize, sourceEncoding.GetMaxCharCount(MaxByteBufferSize));
_charBuffer = new ArraySegment<char>(
ArrayPool<char>.Shared.Rent(maxCharBufferSize),
0,
count: 0);
_overflowBuffer = new ArraySegment<byte>(
ArrayPool<byte>.Shared.Rent(OverflowBufferSize),
0,
count: 0);
_decoder = sourceEncoding.GetDecoder();
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
internal int ByteBufferCount => _byteBuffer.Count;
internal int CharBufferCount => _charBuffer.Count;
internal int OverflowCount => _overflowBuffer.Count;
public override void Flush()
=> throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ThrowArgumentOutOfRangeException(buffer, offset, count);
if (count == 0)
{
return 0;
}
var readBuffer = new ArraySegment<byte>(buffer, offset, count);
if (_overflowBuffer.Count > 0)
{
var bytesToCopy = Math.Min(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;
}
if (_charBuffer.Count == 0)
{
// Only read more content from the input stream if we have exhausted all the buffered chars.
await ReadInputChars(cancellationToken);
}
var operationStatus = Utf8.FromUtf16(_charBuffer, readBuffer, out var charsRead, out var bytesWritten, isFinalBlock: false);
_charBuffer = _charBuffer.Slice(charsRead);
switch (operationStatus)
{
case OperationStatus.Done:
return bytesWritten;
case OperationStatus.DestinationTooSmall:
if (bytesWritten != 0)
{
return bytesWritten;
}
// Overflow buffer is always empty when we get here and we can use it's full length to write contents to.
Utf8.FromUtf16(_charBuffer, _overflowBuffer.Array, out var overFlowChars, out var overflowBytes, isFinalBlock: false);
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);
_overflowBuffer = new ArraySegment<byte>(
_overflowBuffer.Array,
readBuffer.Count,
overflowBytes - readBuffer.Count);
Debug.Assert(_overflowBuffer.Count != 0);
return readBuffer.Count;
default:
Debug.Fail("We should never see this");
throw new InvalidOperationException();
}
}
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 in to
// the segment that follows.
Buffer.BlockCopy(
_byteBuffer.Array,
_byteBuffer.Offset,
_byteBuffer.Array,
0,
_byteBuffer.Count);
var readBytes = await _stream.ReadAsync(_byteBuffer.Array.AsMemory(_byteBuffer.Count), cancellationToken);
_byteBuffer = new ArraySegment<byte>(_byteBuffer.Array, 0, _byteBuffer.Count + readBytes);
Debug.Assert(_charBuffer.Count == 0, "We should only expect to read more input chars once all buffered content is read");
_decoder.Convert(
_byteBuffer.AsSpan(),
_charBuffer.Array,
flush: readBytes == 0,
out var bytesUsed,
out var charsUsed,
out _);
_byteBuffer = _byteBuffer.Slice(bytesUsed);
_charBuffer = new ArraySegment<char>(_charBuffer.Array, 0, charsUsed);
}
private static void ThrowArgumentOutOfRangeException(byte[] buffer, int offset, int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
if (offset < 0 || offset >= buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
if (buffer.Length - offset < count)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
ArrayPool<char>.Shared.Return(_charBuffer.Array);
ArrayPool<byte>.Shared.Return(_byteBuffer.Array);
ArrayPool<byte>.Shared.Return(_overflowBuffer.Array);
}
}
}
}

View File

@ -1,181 +0,0 @@
// 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.Buffers;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
{
internal sealed class TranscodingWriteStream : Stream
{
internal const int MaxCharBufferSize = 4096;
internal const int MaxByteBufferSize = 4 * MaxCharBufferSize;
private readonly int _maxByteBufferSize;
private readonly Stream _stream;
private readonly Decoder _decoder;
private readonly Encoder _encoder;
private readonly char[] _charBuffer;
private int _charsDecoded;
private bool _disposed;
public TranscodingWriteStream(Stream stream, Encoding targetEncoding)
{
_stream = stream;
_charBuffer = ArrayPool<char>.Shared.Rent(MaxCharBufferSize);
// 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));
_decoder = Encoding.UTF8.GetDecoder();
_encoder = targetEncoding.GetEncoder();
}
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position { get; set; }
public override void Flush()
=> throw new NotSupportedException();
public override async Task FlushAsync(CancellationToken cancellationToken)
{
await _stream.FlushAsync(cancellationToken);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
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)
{
ThrowArgumentException(buffer, offset, count);
var bufferSegment = new ArraySegment<byte>(buffer, offset, count);
return WriteAsync(bufferSegment, cancellationToken);
}
private async Task WriteAsync(
ArraySegment<byte> bufferSegment,
CancellationToken cancellationToken)
{
var decoderCompleted = false;
while (!decoderCompleted)
{
_decoder.Convert(
bufferSegment,
_charBuffer.AsSpan(_charsDecoded),
flush: false,
out var bytesDecoded,
out var charsDecoded,
out decoderCompleted);
_charsDecoded += charsDecoded;
bufferSegment = bufferSegment.Slice(bytesDecoded);
if (!decoderCompleted)
{
await WriteBufferAsync(cancellationToken);
}
}
}
private async Task WriteBufferAsync(CancellationToken cancellationToken)
{
var encoderCompleted = false;
var charsWritten = 0;
var byteBuffer = ArrayPool<byte>.Shared.Rent(_maxByteBufferSize);
while (!encoderCompleted && charsWritten < _charsDecoded)
{
_encoder.Convert(
_charBuffer.AsSpan(charsWritten, _charsDecoded - charsWritten),
byteBuffer,
flush: false,
out var charsEncoded,
out var bytesUsed,
out encoderCompleted);
await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken);
charsWritten += charsEncoded;
}
ArrayPool<byte>.Shared.Return(byteBuffer);
// At this point, we've written all the buffered chars to the underlying Stream.
_charsDecoded = 0;
}
private static void ThrowArgumentException(byte[] buffer, int offset, int count)
{
if (count <= 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
if (offset < 0 || offset >= buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
if (buffer.Length - offset < count)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
ArrayPool<char>.Shared.Return(_charBuffer);
}
}
public async Task FinalWriteAsync(CancellationToken cancellationToken)
{
// First write any buffered content
await WriteBufferAsync(cancellationToken);
// Now flush the encoder.
var byteBuffer = ArrayPool<byte>.Shared.Rent(_maxByteBufferSize);
var encoderCompleted = false;
while (!encoderCompleted)
{
_encoder.Convert(
Array.Empty<char>(),
byteBuffer,
flush: true,
out _,
out var bytesUsed,
out encoderCompleted);
await _stream.WriteAsync(byteBuffer.AsMemory(0, bytesUsed), cancellationToken);
}
ArrayPool<byte>.Shared.Return(byteBuffer);
}
}
}

View File

@ -2,16 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Formatters.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
@ -71,50 +67,56 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Log.JsonResultExecuting(_logger, result.Value);
var value = result.Value;
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
{
Log.BufferingAsyncEnumerable(_logger, value);
value = await reader(value);
}
var objectType = value?.GetType() ?? typeof(object);
// Keep this code in sync with SystemTextJsonOutputFormatter
var writeStream = GetWriteStream(context.HttpContext, resolvedContentTypeEncoding);
try
var responseStream = response.Body;
if (resolvedContentTypeEncoding.CodePage == Encoding.UTF8.CodePage)
{
var value = result.Value;
if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
{
Log.BufferingAsyncEnumerable(_logger, value);
value = await reader(value);
}
var type = value?.GetType() ?? typeof(object);
await JsonSerializer.SerializeAsync(writeStream, value, type, jsonSerializerOptions);
// The transcoding streams use Encoders and Decoders that have internal buffers. We need to flush these
// when there is no more data to be written. Stream.FlushAsync isn't suitable since it's
// acceptable to Flush a Stream (multiple times) prior to completion.
if (writeStream is TranscodingWriteStream transcodingStream)
{
await transcodingStream.FinalWriteAsync(CancellationToken.None);
}
await writeStream.FlushAsync();
await JsonSerializer.SerializeAsync(responseStream, value, objectType, jsonSerializerOptions);
await responseStream.FlushAsync();
}
finally
else
{
if (writeStream is TranscodingWriteStream transcodingStream)
// JsonSerializer only emits UTF8 encoded output, but we need to write the response in the encoding specified by
// selectedEncoding
var transcodingStream = Encoding.CreateTranscodingStream(response.Body, resolvedContentTypeEncoding, Encoding.UTF8, leaveOpen: true);
ExceptionDispatchInfo exceptionDispatchInfo = null;
try
{
await transcodingStream.DisposeAsync();
await JsonSerializer.SerializeAsync(transcodingStream, value, objectType, jsonSerializerOptions);
await transcodingStream.FlushAsync();
}
catch (Exception ex)
{
// TranscodingStream may write to the inner stream as part of it's disposal.
// We do not want this exception "ex" to be eclipsed by any exception encountered during the write. We will stash it and
// explicitly rethrow it during the finally block.
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
}
finally
{
try
{
await transcodingStream.DisposeAsync();
}
catch when (exceptionDispatchInfo != null)
{
}
exceptionDispatchInfo?.Throw();
}
}
}
private Stream GetWriteStream(HttpContext httpContext, Encoding selectedEncoding)
{
if (selectedEncoding.CodePage == Encoding.UTF8.CodePage)
{
// JsonSerializer does not write a BOM. Therefore we do not have to handle it
// in any special way.
return httpContext.Response.Body;
}
return new TranscodingWriteStream(httpContext.Response.Body, selectedEncoding);
}
private JsonSerializerOptions GetSerializerOptions(JsonResult result)
{
var serializerSettings = result.SerializerSettings;

View File

@ -110,6 +110,28 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal("abcd", stringValue);
}
[Fact]
public async Task JsonFormatterReadsNonUtf8Content()
{
// Arrange
var content = "☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☮☯☰☱☲☳☴☵☶☷☸";
var formatter = GetInputFormatter();
var contentBytes = Encoding.Unicode.GetBytes($"\"{content}\"");
var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-16");
var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.False(result.HasError);
var stringValue = Assert.IsType<string>(result.Model);
Assert.Equal(content, stringValue);
Assert.True(httpContext.Request.Body.CanRead, "Verify that the request stream hasn't been disposed");
}
[Fact]
public virtual async Task JsonFormatter_EscapedKeys_Bracket()
{

View File

@ -108,6 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase);
Assert.True(body.CanWrite, "Response body should not be disposed.");
}
[Fact]

View File

@ -1,8 +1,10 @@
// 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.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
@ -55,6 +57,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task WriteResponseBodyAsync_WithNonUtf8Encoding_FormattingErrorsAreThrown()
{
// Arrange
var formatter = GetOutputFormatter();
var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-16");
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-16", isDefaultEncoding: true);
var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);
var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(Person),
new ThrowingFormatterModel())
{
ContentType = new StringSegment(mediaType.ToString()),
};
// Act & Assert
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-16")));
}
private class Person
{
public string Name { get; set; }
@ -63,5 +89,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
public Person Parent { get; set; }
}
[JsonConverter(typeof(ThrowingFormatterPersonConverter))]
private class ThrowingFormatterModel
{
}
private class ThrowingFormatterPersonConverter : JsonConverter<ThrowingFormatterModel>
{
public override ThrowingFormatterModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ThrowingFormatterModel value, JsonSerializerOptions options)
{
throw new TimeZoneNotFoundException();
}
}
}
}

View File

@ -1,254 +0,0 @@
// 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.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
{
public class TranscodingReadStreamTest
{
[Fact]
public async Task ReadAsync_SingleByte()
{
// Arrange
var input = "Hello world";
var encoding = Encoding.Unicode;
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
var bytes = new byte[4];
// Act
var readBytes = await stream.ReadAsync(bytes, 0, 1);
// Assert
Assert.Equal(1, readBytes);
Assert.Equal((byte)'H', bytes[0]);
Assert.Equal(0, bytes[1]);
Assert.Equal(0, stream.ByteBufferCount);
Assert.Equal(10, stream.CharBufferCount);
Assert.Equal(0, stream.OverflowCount);
}
[Fact]
public async Task ReadAsync_FillsBuffer()
{
// Arrange
var input = "Hello world";
var encoding = Encoding.Unicode;
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
var bytes = new byte[3];
var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length));
// Act
var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
// Assert
Assert.Equal(3, readBytes);
Assert.Equal(expected, bytes);
Assert.Equal(0, stream.ByteBufferCount);
Assert.Equal(8, stream.CharBufferCount);
Assert.Equal(0, stream.OverflowCount);
}
[Fact]
public async Task ReadAsync_CompletedInSecondIteration()
{
// Arrange
var input = new string('A', 1024 + 10);
var encoding = Encoding.Unicode;
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
var bytes = new byte[1024];
var expected = Encoding.UTF8.GetBytes(input.Substring(0, bytes.Length));
// Act
var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
// Assert
Assert.Equal(bytes.Length, readBytes);
Assert.Equal(expected, bytes);
Assert.Equal(0, stream.ByteBufferCount);
Assert.Equal(10, stream.CharBufferCount);
Assert.Equal(0, stream.OverflowCount);
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
Assert.Equal(10, readBytes);
Assert.Equal(0, stream.ByteBufferCount);
Assert.Equal(0, stream.CharBufferCount);
Assert.Equal(0, stream.OverflowCount);
}
[Fact]
public async Task ReadAsync_WithOverflowBuffer()
{
// Arrange
// Test ensures that the overflow buffer works correctly
var input = "☀";
var encoding = Encoding.Unicode;
using var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
var bytes = new byte[1];
var expected = Encoding.UTF8.GetBytes(input);
// Act
var readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
// Assert
Assert.Equal(1, readBytes);
Assert.Equal(expected[0], bytes[0]);
Assert.Equal(0, stream.ByteBufferCount);
Assert.Equal(0, stream.CharBufferCount);
Assert.Equal(2, stream.OverflowCount);
bytes = new byte[expected.Length - 1];
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
Assert.Equal(bytes.Length, readBytes);
Assert.Equal(0, stream.ByteBufferCount);
Assert.Equal(0, stream.CharBufferCount);
Assert.Equal(0, stream.OverflowCount);
readBytes = await stream.ReadAsync(bytes, 0, bytes.Length);
Assert.Equal(0, readBytes);
}
public static TheoryData<string> ReadAsync_WithOverflowBuffer_AtBoundariesData => new TheoryData<string>
{
new string('a', TranscodingReadStream.MaxCharBufferSize - 1) + "☀",
new string('a', TranscodingReadStream.MaxCharBufferSize - 2) + "☀",
new string('a', TranscodingReadStream.MaxCharBufferSize) + "☀",
};
[Theory]
[MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))]
public Task ReadAsync_WithOverflowBuffer_WithBufferSize1(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1);
[Theory]
[MemberData(nameof(ReadAsync_WithOverflowBuffer_AtBoundariesData))]
public Task ReadAsync_WithOverflowBuffer_WithBufferSize2(string input) => ReadAsync_WithOverflowBufferAtCharBufferBoundaries(input, bufferSize: 1);
private static async Task<TranscodingReadStream> ReadAsync_WithOverflowBufferAtCharBufferBoundaries(string input, int bufferSize)
{
// Arrange
// Test ensures that the overflow buffer works correctly
var encoding = Encoding.Unicode;
var stream = new TranscodingReadStream(new MemoryStream(encoding.GetBytes(input)), encoding);
var bytes = new byte[1];
var expected = Encoding.UTF8.GetBytes(input);
// Act
int read;
var buffer = new byte[bufferSize];
var actual = new List<byte>();
while ((read = await stream.ReadAsync(buffer)) != 0)
{
actual.AddRange(buffer);
}
Assert.Equal(expected, actual);
return stream;
}
public static TheoryData ReadAsyncInputLatin =>
GetLatinTextInput(TranscodingReadStream.MaxCharBufferSize, TranscodingReadStream.MaxByteBufferSize);
public static TheoryData ReadAsyncInputUnicode =>
GetUnicodeText(TranscodingReadStream.MaxCharBufferSize);
internal static TheoryData GetLatinTextInput(int maxCharBufferSize, int maxByteBufferSize)
{
return new TheoryData<string>
{
"Hello world",
string.Join(string.Empty, Enumerable.Repeat("AB", 9000)),
new string('A', count: maxByteBufferSize),
new string('A', count: maxCharBufferSize),
new string('A', count: maxByteBufferSize + 1),
new string('A', count: maxCharBufferSize + 1),
};
}
internal static TheoryData GetUnicodeText(int maxCharBufferSize)
{
return new TheoryData<string>
{
new string('Æ', count: 7),
new string('A', count: maxCharBufferSize - 1) + 'Æ',
"AbĀāĂ㥹ĆŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſAbc",
"Abcஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஷஸஹ",
"☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸",
new string('Æ', count: 64 * 1024),
new string('Æ', count: 64 * 1024 + 1),
"pingüino",
new string('ऄ', count: maxCharBufferSize + 1), // This uses 3 bytes to represent in UTF8
};
}
[Theory]
[MemberData(nameof(ReadAsyncInputLatin))]
[MemberData(nameof(ReadAsyncInputUnicode))]
public Task ReadAsync_Works_WhenInputIs_UTF32(string message)
{
var sourceEncoding = Encoding.UTF32;
return ReadAsyncTest(sourceEncoding, message);
}
[Theory]
[MemberData(nameof(ReadAsyncInputLatin))]
[MemberData(nameof(ReadAsyncInputUnicode))]
public Task ReadAsync_Works_WhenInputIs_Unicode(string message)
{
var sourceEncoding = Encoding.Unicode;
return ReadAsyncTest(sourceEncoding, message);
}
[Theory]
[MemberData(nameof(ReadAsyncInputLatin))]
[MemberData(nameof(ReadAsyncInputUnicode))]
public Task ReadAsync_Works_WhenInputIs_UTF7(string message)
{
var sourceEncoding = Encoding.UTF7;
return ReadAsyncTest(sourceEncoding, message);
}
[Theory]
[MemberData(nameof(ReadAsyncInputLatin))]
public Task ReadAsync_Works_WhenInputIs_WesternEuropeanEncoding(string message)
{
// Arrange
var sourceEncoding = Encoding.GetEncoding(28591);
return ReadAsyncTest(sourceEncoding, message);
}
[Theory]
[MemberData(nameof(ReadAsyncInputLatin))]
public Task ReadAsync_Works_WhenInputIs_ASCII(string message)
{
// Arrange
var sourceEncoding = Encoding.ASCII;
return ReadAsyncTest(sourceEncoding, message);
}
private static async Task ReadAsyncTest(Encoding sourceEncoding, string message)
{
var input = $"{{ \"Message\": \"{message}\" }}";
var stream = new MemoryStream(sourceEncoding.GetBytes(input));
var transcodingStream = new TranscodingReadStream(stream, sourceEncoding);
var model = await JsonSerializer.DeserializeAsync(transcodingStream, typeof(TestModel));
var testModel = Assert.IsType<TestModel>(model);
Assert.Equal(message, testModel.Message);
}
public class TestModel
{
public string Message { get; set; }
}
}
}

View File

@ -1,87 +0,0 @@
// 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.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters.Json
{
public class TranscodingWriteStreamTest
{
public static TheoryData WriteAsyncInputLatin =>
TranscodingReadStreamTest.GetLatinTextInput(TranscodingWriteStream.MaxCharBufferSize, TranscodingWriteStream.MaxByteBufferSize);
public static TheoryData WriteAsyncInputUnicode =>
TranscodingReadStreamTest.GetUnicodeText(TranscodingWriteStream.MaxCharBufferSize);
[Theory]
[MemberData(nameof(WriteAsyncInputLatin))]
[MemberData(nameof(WriteAsyncInputUnicode))]
public Task WriteAsync_Works_WhenOutputIs_UTF32(string message)
{
var targetEncoding = Encoding.UTF32;
return WriteAsyncTest(targetEncoding, message);
}
[Theory]
[MemberData(nameof(WriteAsyncInputLatin))]
[MemberData(nameof(WriteAsyncInputUnicode))]
public Task WriteAsync_Works_WhenOutputIs_Unicode(string message)
{
var targetEncoding = Encoding.Unicode;
return WriteAsyncTest(targetEncoding, message);
}
[Theory]
[MemberData(nameof(WriteAsyncInputLatin))]
public Task WriteAsync_Works_WhenOutputIs_UTF7(string message)
{
var targetEncoding = Encoding.UTF7;
return WriteAsyncTest(targetEncoding, message);
}
[Theory]
[MemberData(nameof(WriteAsyncInputLatin))]
public Task WriteAsync_Works_WhenOutputIs_WesternEuropeanEncoding(string message)
{
// Arrange
var targetEncoding = Encoding.GetEncoding(28591);
return WriteAsyncTest(targetEncoding, message);
}
[Theory]
[MemberData(nameof(WriteAsyncInputLatin))]
public Task WriteAsync_Works_WhenOutputIs_ASCII(string message)
{
// Arrange
var targetEncoding = Encoding.ASCII;
return WriteAsyncTest(targetEncoding, message);
}
private static async Task WriteAsyncTest(Encoding targetEncoding, string message)
{
var expected = $"{{\"Message\":\"{JavaScriptEncoder.Default.Encode(message)}\"}}";
var model = new TestModel { Message = message };
var stream = new MemoryStream();
var transcodingStream = new TranscodingWriteStream(stream, targetEncoding);
await JsonSerializer.SerializeAsync(transcodingStream, model, model.GetType());
await transcodingStream.FinalWriteAsync(default);
await transcodingStream.FlushAsync();
var actual = targetEncoding.GetString(stream.ToArray());
Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase);
}
private class TestModel
{
public string Message { get; set; }
}
}
}

View File

@ -382,7 +382,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
return httpContext;
}
private static ActionContext GetActionContext()
protected static ActionContext GetActionContext()
{
return new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
}

View File

@ -1,9 +1,14 @@
// 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.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
@ -17,9 +22,44 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Options.Create(new MvcOptions()));
}
[Fact]
public async Task WriteResponseBodyAsync_WithNonUtf8Encoding_FormattingErrorsAreThrown()
{
// Arrange
var context = GetActionContext();
var result = new JsonResult(new ThrowingFormatterModel())
{
ContentType = "application/json; charset=utf-16",
};
var executor = CreateExecutor();
// Act & Assert
await Assert.ThrowsAsync<TimeZoneNotFoundException>(() => executor.ExecuteAsync(context, result));
}
protected override object GetIndentedSettings()
{
return new JsonSerializerOptions { WriteIndented = true };
}
[JsonConverter(typeof(ThrowingFormatterPersonConverter))]
private class ThrowingFormatterModel
{
}
private class ThrowingFormatterPersonConverter : JsonConverter<ThrowingFormatterModel>
{
public override ThrowingFormatterModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ThrowingFormatterModel value, JsonSerializerOptions options)
{
throw new TimeZoneNotFoundException();
}
}
}
}