diff --git a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs index 011dd56..6319d0b 100644 --- a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs +++ b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs @@ -103,6 +103,12 @@ public class JsonEventFormatter : CloudEventFormatter /// protected JsonDocumentOptions DocumentOptions { get; } + /// + /// Whether non-UTF8 JSON should be transcoded to UTF-8 before parsing when the target framework supports it. + /// Derived classes may override this to exercise the fallback path. + /// + protected virtual bool UseTranscodingStreamForNonUtf8 => true; + /// /// Creates a JsonEventFormatter that uses the default /// and for serializing and parsing. @@ -193,15 +199,20 @@ private async Task ReadDocumentAsync(Stream data, ContentType? con if (encoding is not UTF8Encoding) { #if NET5_0_OR_GREATER - data = Encoding.CreateTranscodingStream(data, encoding, Encoding.UTF8); -#else - using var reader = new StreamReader(data, encoding); - var json = async - ? await reader.ReadToEndAsync().ConfigureAwait(false) - : reader.ReadToEnd(); - - return JsonDocument.Parse(json, DocumentOptions); + if (UseTranscodingStreamForNonUtf8) + { + data = Encoding.CreateTranscodingStream(data, encoding, Encoding.UTF8); + } + else #endif + { + using var reader = new StreamReader(data, encoding); + var json = async + ? await reader.ReadToEndAsync().ConfigureAwait(false) + : reader.ReadToEnd(); + + return JsonDocument.Parse(json, DocumentOptions); + } } return async diff --git a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs index 587d6fc..89d35b2 100644 --- a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs @@ -538,6 +538,7 @@ public async Task DecodeStructuredModeMessageAsync_Minimal(string charset) [Theory] [InlineData("utf-8")] + [InlineData("utf-16")] [InlineData("iso-8859-1")] public void DecodeStructuredModeMessage_Minimal(string charset) { @@ -556,6 +557,31 @@ public void DecodeStructuredModeMessage_Minimal(string charset) Assert.Equal("test-type", cloudEvent.Type); Assert.Equal("test-id", cloudEvent.Id); Assert.Equal(SampleUri, cloudEvent.Source); + Assert.Equal(NonAsciiValue, cloudEvent["text"]); + } + + [Theory] + [InlineData("utf-8")] + [InlineData("utf-16")] + [InlineData("iso-8859-1")] + public async Task DecodeStructuredModeMessage_Minimal_ForcedNonUtf8Fallback(string charset) + { + var obj = new JObject + { + ["specversion"] = "1.0", + ["type"] = "test-type", + ["id"] = "test-id", + ["source"] = SampleUriText, + ["text"] = NonAsciiValue + }; + var bytes = Encoding.GetEncoding(charset).GetBytes(obj.ToString()); + var stream = new MemoryStream(bytes); + var formatter = new NonTranscodingJsonEventFormatter(); + var cloudEvent = formatter.DecodeStructuredModeMessage(stream, new ContentType($"application/cloudevents+json; charset={charset}"), null); + Assert.Equal("test-type", cloudEvent.Type); + Assert.Equal("test-id", cloudEvent.Id); + Assert.Equal(SampleUri, cloudEvent.Source); + Assert.Equal(NonAsciiValue, cloudEvent["text"]); } [Fact] @@ -1218,6 +1244,11 @@ private static IReadOnlyList DecodeBatchModeMessage(Newtonsoft.Json. return formatter.DecodeBatchModeMessage(bytes, s_jsonCloudEventBatchContentType, null); } + private sealed class NonTranscodingJsonEventFormatter : JsonEventFormatter + { + protected override bool UseTranscodingStreamForNonUtf8 => false; + } + private sealed class YearMonthDayConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>