diff --git a/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonReaderExtensions.cs b/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonReaderExtensions.cs new file mode 100644 index 00000000000..7b40b3f1b30 --- /dev/null +++ b/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonReaderExtensions.cs @@ -0,0 +1,40 @@ +// 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.Json; + +namespace NuGet.ProjectModel +{ + internal static class Utf8JsonReaderExtensions + { + internal static string ReadTokenAsString(this ref Utf8JsonReader reader) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + return bool.TrueString; + case JsonTokenType.False: + return bool.FalseString; + case JsonTokenType.Number: + return reader.ReadNumberAsString(); + case JsonTokenType.String: + return reader.GetString(); + case JsonTokenType.None: + case JsonTokenType.Null: + return null; + default: + throw new InvalidCastException(); + } + } + + private static string ReadNumberAsString(this ref Utf8JsonReader reader) + { + if (reader.TryGetInt64(out long value)) + { + return value.ToString(); + } + return reader.GetDouble().ToString(); + } + } +} diff --git a/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonStreamReader.cs b/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonStreamReader.cs new file mode 100644 index 00000000000..0dc89846b86 --- /dev/null +++ b/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonStreamReader.cs @@ -0,0 +1,273 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace NuGet.ProjectModel +{ + /// + /// This struct is used to read over a memeory stream in parts, in order to avoid reading the entire stream into memory. + /// It functions as a wrapper around , while maintaining a stream and a buffer to read from. + /// + internal ref struct Utf8JsonStreamReader + { + private static readonly char[] DelimitedStringDelimiters = [' ', ',']; + private static readonly byte[] Utf8Bom = [0xEF, 0xBB, 0xBF]; + + private const int BufferSizeDefault = 16 * 1024; + private const int MinBufferSize = 1024; + private Utf8JsonReader _reader; +#pragma warning disable CA2213 // Disposable fields should be disposed + private Stream _stream; +#pragma warning restore CA2213 // Disposable fields should be disposed + // The buffer is used to read from the stream in chunks. + private byte[] _buffer; + private bool _disposed; + private ArrayPool _bufferPool; + private int _bufferUsed = 0; + + internal Utf8JsonStreamReader(Stream stream, int bufferSize = BufferSizeDefault, ArrayPool arrayPool = null) + { + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (bufferSize < MinBufferSize) + { + throw new ArgumentException($"Buffer size must be at least {MinBufferSize} bytes", nameof(bufferSize)); + } + + _bufferPool = arrayPool ?? ArrayPool.Shared; + _buffer = _bufferPool.Rent(bufferSize); + _disposed = false; + _stream = stream; + _stream.Read(_buffer, 0, 3); + if (!Utf8Bom.AsSpan().SequenceEqual(_buffer.AsSpan(0, 3))) + { + _bufferUsed = 3; + } + + var iniialJsonReaderState = new JsonReaderState(new JsonReaderOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }); + + ReadStreamIntoBuffer(iniialJsonReaderState); + _reader.Read(); + } + + internal bool IsFinalBlock => _reader.IsFinalBlock; + + internal JsonTokenType TokenType => _reader.TokenType; + + internal bool ValueTextEquals(ReadOnlySpan utf8Text) => _reader.ValueTextEquals(utf8Text); + + internal bool TryGetInt32(out int value) => _reader.TryGetInt32(out value); + + internal string GetString() => _reader.GetString(); + + internal bool GetBoolean() => _reader.GetBoolean(); + + internal int GetInt32() => _reader.GetInt32(); + + internal bool Read() + { + ThrowExceptionIfDisposed(); + + bool wasRead; + while (!(wasRead = _reader.Read()) && !_reader.IsFinalBlock) + { + GetMoreBytesFromStream(); + } + return wasRead; + } + + internal void Skip() + { + ThrowExceptionIfDisposed(); + + bool wasSkipped; + while (!(wasSkipped = _reader.TrySkip()) && !_reader.IsFinalBlock) + { + GetMoreBytesFromStream(); + } + if (!wasSkipped) + { + _reader.Skip(); + } + } + + internal string ReadNextTokenAsString() + { + ThrowExceptionIfDisposed(); + + if (Read()) + { + return _reader.ReadTokenAsString(); + } + + return null; + } + + internal IList ReadStringArrayAsIList(IList strings = null) + { + if (TokenType == JsonTokenType.StartArray) + { + while (Read() && TokenType != JsonTokenType.EndArray) + { + string value = _reader.ReadTokenAsString(); + + strings = strings ?? new List(); + + strings.Add(value); + } + } + return strings; + } + + internal IReadOnlyList ReadDelimitedString() + { + ThrowExceptionIfDisposed(); + + if (Read()) + { + switch (TokenType) + { + case JsonTokenType.String: + var value = GetString(); + + return value.Split(DelimitedStringDelimiters, StringSplitOptions.RemoveEmptyEntries); + + default: + var invalidCastException = new InvalidCastException(); + throw new JsonException(invalidCastException.Message, invalidCastException); + } + } + + return null; + } + + internal bool ReadNextTokenAsBoolOrFalse() + { + ThrowExceptionIfDisposed(); + + if (Read() && (TokenType == JsonTokenType.False || TokenType == JsonTokenType.True)) + { + return GetBoolean(); + } + return false; + } + + internal IReadOnlyList ReadNextStringOrArrayOfStringsAsReadOnlyList() + { + ThrowExceptionIfDisposed(); + + if (Read()) + { + switch (_reader.TokenType) + { + case JsonTokenType.String: + return new[] { (string)_reader.GetString() }; + + case JsonTokenType.StartArray: + return ReadStringArrayAsReadOnlyListFromArrayStart(); + + case JsonTokenType.StartObject: + return null; + } + } + + return null; + } + + internal IReadOnlyList ReadStringArrayAsReadOnlyListFromArrayStart() + { + ThrowExceptionIfDisposed(); + + List strings = null; + + while (Read() && _reader.TokenType != JsonTokenType.EndArray) + { + string value = _reader.ReadTokenAsString(); + + strings = strings ?? new List(); + + strings.Add(value); + } + + return (IReadOnlyList)strings ?? Array.Empty(); + } + + // This function is called when Read() returns false and we're not already in the final block + private void GetMoreBytesFromStream() + { + if (_reader.BytesConsumed < _bufferUsed) + { + // If the number of bytes consumed by the reader is less than the amount set in the buffer then we have leftover bytes + var oldBuffer = _buffer; + ReadOnlySpan leftover = oldBuffer.AsSpan((int)_reader.BytesConsumed); + _bufferUsed = leftover.Length; + + // If the leftover bytes are the same as the buffer size then we are at capacity and need to double the buffer size + if (leftover.Length == _buffer.Length) + { + _buffer = _bufferPool.Rent(_buffer.Length * 2); + leftover.CopyTo(_buffer); + _bufferPool.Return(oldBuffer, true); + } + else + { + leftover.CopyTo(_buffer); + } + } + else + { + _bufferUsed = 0; + } + + ReadStreamIntoBuffer(_reader.CurrentState); + } + + /// + /// Loops through the stream and reads it into the buffer until the buffer is full or the stream is empty, creates the Utf8JsonReader. + /// + private void ReadStreamIntoBuffer(JsonReaderState jsonReaderState) + { + int bytesRead; + do + { + var spaceLeftInBuffer = _buffer.Length - _bufferUsed; + bytesRead = _stream.Read(_buffer, _bufferUsed, spaceLeftInBuffer); + _bufferUsed += bytesRead; + } + while (bytesRead != 0 && _bufferUsed != _buffer.Length); + _reader = new Utf8JsonReader(_buffer.AsSpan(0, _bufferUsed), isFinalBlock: bytesRead == 0, jsonReaderState); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + byte[] toReturn = _buffer; + _buffer = null!; + _bufferPool.Return(toReturn, true); + } + } + + private void ThrowExceptionIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Utf8JsonStreamReader)); + } + } + } +} diff --git a/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonStreamReaderConverter.cs b/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonStreamReaderConverter.cs new file mode 100644 index 00000000000..167750c3828 --- /dev/null +++ b/src/NuGet.Core/NuGet.ProjectModel/Utf8JsonStreamReaderConverter.cs @@ -0,0 +1,13 @@ +// 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. +namespace NuGet.ProjectModel +{ + /// + /// An abstract class that defines a function for reading a into a + /// + /// + internal abstract class Utf8JsonStreamReaderConverter + { + public abstract T Read(ref Utf8JsonStreamReader reader); + } +} diff --git a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs index 54977dc03b8..310781aa194 100644 --- a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs +++ b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/LockFileFormatTests.cs @@ -213,7 +213,6 @@ public void LockFileFormat_ReadsLockFileWithNoTools() var target = lockFile.Targets.Single(); Assert.Equal(NuGetFramework.Parse("dotnet"), target.TargetFramework); - var runtimeTargetLibrary = target.Libraries.Single(); Assert.Equal("System.Runtime", runtimeTargetLibrary.Name); Assert.Equal(NuGetVersion.Parse("4.0.20-beta-22927"), runtimeTargetLibrary.Version); diff --git a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/Utf8JsonReaderExtensionsTests.cs b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/Utf8JsonReaderExtensionsTests.cs new file mode 100644 index 00000000000..0c60a72b4e3 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/Utf8JsonReaderExtensionsTests.cs @@ -0,0 +1,35 @@ +// 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.Text; +using System.Text.Json; +using Xunit; + +namespace NuGet.ProjectModel.Test +{ + [UseCulture("")] // Fix tests failing on systems with non-English locales + public class Utf8JsonReaderExtensionsTests + { + [Theory] + [InlineData("null", null)] + [InlineData("true", "True")] + [InlineData("false", "False")] + [InlineData("-2", "-2")] + [InlineData("9223372036854775807", "9223372036854775807")] + [InlineData("3.14", "3.14")] + [InlineData("\"b\"", "b")] + public void ReadTokenAsString_WhenValueIsConvertibleToString_ReturnsValueAsString( + string value, + string expectedResult) + { + var json = $"{{\"a\":{value}}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + var reader = new Utf8JsonReader(encodedBytes); + reader.Read(); + reader.Read(); + reader.Read(); + string actualResult = reader.ReadTokenAsString(); + Assert.Equal(expectedResult, actualResult); + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/Utf8JsonStreamReaderTests.cs b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/Utf8JsonStreamReaderTests.cs new file mode 100644 index 00000000000..3193f43c4f5 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.ProjectModel.Test/Utf8JsonStreamReaderTests.cs @@ -0,0 +1,833 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using Moq; +using Xunit; + +namespace NuGet.ProjectModel.Test +{ + [UseCulture("")] // Fix tests failing on systems with non-English locales + public class Utf8JsonStreamReaderTests + { + private static readonly string JsonWithOverflowObject = "{\"object1\":{\"a\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"b\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"c\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"d\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"e\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"f\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"g\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"h\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"i\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"j\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"k\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"l\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"m\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"n\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"o\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"p\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"q\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"r\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"s\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"t\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"u\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\"},\"object2\":{\"a\":\"abcdefghijklmnopqrstuvwxyz\",\"b\":\"abcdefghijklmnopqrstuvwxyz\",\"c\":\"abcdefghijklmnopqrstuvwxyz\",\"d\":\"abcdefghijklmnopqrstuvwxyz\",\"e\":\"abcdefghijklmnopqrstuvwxyz\",\"f\":\"abcdefghijklmnopqrstuvwxyz\",\"g\":\"abcdefghijklmnopqrstuvwxyz\",\"h\":\"abcdefghijklmnopqrstuvwxyz\",\"i\":\"abcdefghijklmnopqrstuvwxyz\",\"j\":\"abcdefghijklmnopqrstuvwxyz\",\"k\":\"abcdefghijklmnopqrstuvwxyz\",\"l\":\"abcdefghijklmnopqrstuvwxyz\",\"m\":\"abcdefghijklmnopqrstuvwxyz\",\"n\":\"abcdefghijklmnopqrstuvwxyz\",\"o\":\"abcdefghijklmnopqrstuvwxyz\",\"p\":\"abcdefghijklmnopqrstuvwxyz\",\"q\":\"abcdefghijklmnopqrstuvwxyz\",\"r\":\"abcdefghijklmnopqrstuvwxyz\",\"s\":\"abcdefghijklmnopqrstuvwxyz\",\"t\":\"abcdefghijklmnopqrstuvwxyz\",\"u\":\"abcdefghijklmnopqrstuvwxyz\"}}"; + private static readonly string JsonWithoutOverflow = "{\"object1\":{\"a\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"b\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"c\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"d\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"e\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"f\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"g\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"h\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"i\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"j\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"k\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"l\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"m\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"n\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"o\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"p\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"q\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"r\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"s\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"t\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"u\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\"}}"; + private static readonly string JsonWithOverflow = "{\"object1\":{\"a\":\"abcdefghijklmnopqrstuvwxyz\",\"b\":\"abcdefghijklmnopqrstuvwxyz\",\"c\":\"abcdefghijklmnopqrstuvwxyz\",\"d\":\"abcdefghijklmnopqrstuvwxyz\",\"e\":\"abcdefghijklmnopqrstuvwxyz\",\"f\":\"abcdefghijklmnopqrstuvwxyz\",\"g\":\"abcdefghijklmnopqrstuvwxyz\",\"h\":\"abcdefghijklmnopqrstuvwxyz\",\"i\":\"abcdefghijklmnopqrstuvwxyz\",\"j\":\"abcdefghijklmnopqrstuvwxyz\",\"k\":\"abcdefghijklmnopqrstuvwxyz\",\"l\":\"abcdefghijklmnopqrstuvwxyz\",\"m\":\"abcdefghijklmnopqrstuvwxyz\",\"n\":\"abcdefghijklmnopqrstuvwxyz\",\"o\":\"abcdefghijklmnopqrstuvwxyz\",\"p\":\"abcdefghijklmnopqrstuvwxyz\",\"q\":\"abcdefghijklmnopqrstuvwxyz\",\"r\":\"abcdefghijklmnopqrstuvwxyz\",\"s\":\"abcdefghijklmnopqrstuvwxyz\",\"t\":\"abcdefghijklmnopqrstuvwxyz\",\"u\":\"abcdefghijklmnopqrstuvwxyz\"}, \"object2\": {\"a\":\"abcdefghijklmnopqrstuvwxyz\",\"b\":\"abcdefghijklmnopqrstuvwxyz\",\"c\":\"abcdefghijklmnopqrstuvwxyz\",\"d\":\"abcdefghijklmnopqrstuvwxyz\",\"e\":\"abcdefghijklmnopqrstuvwxyz\",\"f\":\"abcdefghijklmnopqrstuvwxyz\",\"g\":\"abcdefghijklmnopqrstuvwxyz\",\"h\":\"abcdefghijklmnopqrstuvwxyz\",\"i\":\"abcdefghijklmnopqrstuvwxyz\",\"j\":\"abcdefghijklmnopqrstuvwxyz\",\"k\":\"abcdefghijklmnopqrstuvwxyz\",\"l\":\"abcdefghijklmnopqrstuvwxyz\",\"m\":\"abcdefghijklmnopqrstuvwxyz\",\"n\":\"abcdefghijklmnopqrstuvwxyz\",\"o\":\"abcdefghijklmnopqrstuvwxyz\",\"p\":\"abcdefghijklmnopqrstuvwxyz\",\"q\":\"abcdefghijklmnopqrstuvwxyz\",\"r\":\"abcdefghijklmnopqrstuvwxyz\",\"s\":\"abcdefghijklmnopqrstuvwxyz\",\"t\":\"abcdefghijklmnopqrstuvwxyz\",\"u\":\"abcdefghijklmnopqrstuvwxyz\"}, \"object3\":{\"a\":\"abcdefghijklmnopqrstuvwxyz\",\"b\":\"abcdefghijklmnopqrstuvwxyz\",\"c\":\"abcdefghijklmnopqrstuvwxyz\",\"d\":\"abcdefghijklmnopqrstuvwxyz\",\"e\":\"abcdefghijklmnopqrstuvwxyz\",\"f\":\"abcdefghijklmnopqrstuvwxyz\",\"g\":\"abcdefghijklmnopqrstuvwxyz\",\"h\":\"abcdefghijklmnopqrstuvwxyz\",\"i\":\"abcdefghijklmnopqrstuvwxyz\",\"j\":\"abcdefghijklmnopqrstuvwxyz\",\"k\":\"abcdefghijklmnopqrstuvwxyz\",\"l\":\"abcdefghijklmnopqrstuvwxyz\",\"m\":\"abcdefghijklmnopqrstuvwxyz\",\"n\":\"abcdefghijklmnopqrstuvwxyz\",\"o\":\"abcdefghijklmnopqrstuvwxyz\",\"p\":\"abcdefghijklmnopqrstuvwxyz\",\"q\":\"abcdefghijklmnopqrstuvwxyz\",\"r\":\"abcdefghijklmnopqrstuvwxyz\",\"s\":\"abcdefghijklmnopqrstuvwxyz\",\"t\":\"abcdefghijklmnopqrstuvwxyz\",\"u\":\"abcdefghijklmnopqrstuvwxyz\"}}"; + private static readonly string SmallJson = "{\"object1\":{\"a\":\"abcdefghijklmnopqrstuvwxyz\"}}"; + + [Fact] + public void Utf8JsonStreamReaderCtr_WhenStreamIsNull_Throws() + { + Assert.Throws(() => + { + using var reader = new Utf8JsonStreamReader(null); + }); + } + + [Fact] + public void Utf8JsonStreamReaderCtr_WhenBufferToSmall_Throws() + { + Assert.Throws(() => + { + var json = "{}"; + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) + using (var reader = new Utf8JsonStreamReader(stream, 10)) + { + } + }); + } + + [Fact] + public void Utf8JsonStreamReaderCtr_WhenStreamStartsWithUtf8Bom_SkipThem() + { + var json = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()) + "{}"; + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(5, stream.Position); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + } + } + + [Fact] + public void Utf8JsonStreamReaderCtr_WhenStreamStartsWithoutUtf8Bom_ReadFromStart() + { + var json = "{}"; + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(2, stream.Position); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + } + } + + [Fact] + public void Utf8JsonStreamReaderCtr_WhenReadingWithOverflow_FinalBlockFalse() + { + var json = Encoding.UTF8.GetBytes(JsonWithOverflowObject); + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024)) + { + Assert.False(reader.IsFinalBlock); + } + } + + [Fact] + public void Read_WhenReadingMalfornedJsonString_Throws() + { + var json = Encoding.UTF8.GetBytes("{\"a\":\"string}"); + + Assert.ThrowsAny(() => + { + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.True(reader.IsFinalBlock); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.Read(); + } + }); + } + + [Fact] + public void Read_WhenReadingMalfornedJson_Throws() + { + var json = Encoding.UTF8.GetBytes("{\"a\":\"string\"}ohno"); + Assert.ThrowsAny(() => + { + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.True(reader.IsFinalBlock); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.Read(); + reader.Read(); + reader.Read(); + reader.Read(); + } + }); + } + + [Fact] + public void Read_WhenReadingSmallJson_Read() + { + var json = Encoding.UTF8.GetBytes(SmallJson); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.True(reader.IsFinalBlock); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.String, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + } + } + + [Fact] + public void Read_WhenReadingSmallJsonPastEnd_Read() + { + var json = Encoding.UTF8.GetBytes(SmallJson); + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.True(reader.IsFinalBlock); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.String, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + Assert.False(reader.Read()); + } + } + + [Fact] + public void Read_WhenReadingWithoutOverflow_Read() + { + var json = Encoding.UTF8.GetBytes(JsonWithoutOverflow); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + } + } + + [Fact] + public void Read_WhenReadingWithOverflow_ReadNextBuffer() + { + var json = Encoding.UTF8.GetBytes(JsonWithOverflowObject); + var mock = SetupMockArrayBuffer(); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024, mock.Object)) + { + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "r") + { + break; + } + } + reader.Read(); + mock.Verify(m => m.Rent(1024), Times.Exactly(1)); + Assert.Equal(JsonTokenType.String, reader.TokenType); + Assert.Equal("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", reader.GetString()); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + Assert.Equal("s", reader.GetString()); + } + } + + [Fact] + public void Read_WhenReadingWithLargeToken_ResizeBuffer() + { + var json = Encoding.UTF8.GetBytes("{\"largeToken\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"smallToken\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\"}"); + var mock = SetupMockArrayBuffer(); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024, mock.Object)) + { + reader.Read(); + reader.Read(); + + mock.Verify(m => m.Rent(1024), Times.Exactly(1)); + mock.Verify(m => m.Rent(2048), Times.Exactly(1)); + mock.Verify(m => m.Return(It.IsAny(), true), Times.Exactly(1)); + Assert.Equal(JsonTokenType.String, reader.TokenType); + Assert.Equal("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", + reader.GetString()); + } + } + + [Fact] + public void Read_WhenReadingWithLargeTokenReadPastFinal() + { + var json = Encoding.UTF8.GetBytes("{\"largeToken\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\",\"smallToken\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\"}"); + var mock = SetupMockArrayBuffer(); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024, mock.Object)) + { + reader.Read(); + reader.Read(); + reader.Read(); + reader.Read(); + reader.Read(); + mock.Verify(m => m.Rent(1024), Times.Exactly(1)); + mock.Verify(m => m.Rent(2048), Times.Exactly(1)); + mock.Verify(m => m.Return(It.IsAny(), true), Times.Exactly(1)); + Assert.False(reader.Read()); + } + } + + [Fact] + public void Read_WhenReadingWithOverflowToBufferSize_LoadNextBuffer() + { + var json = Encoding.UTF8.GetBytes("{\"largeToken\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst\",\"smallToken\":\"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\"}"); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024)) + { + reader.Read(); + reader.Read(); + reader.Read(); + + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + Assert.Equal("smallToken", reader.GetString()); + Assert.True(reader.Read()); + Assert.Equal(JsonTokenType.String, reader.TokenType); + Assert.Equal("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", reader.GetString()); + } + } + + [Fact] + public void Dispose_NoErrors() + { + var json = Encoding.UTF8.GetBytes(SmallJson); + var mock = SetupMockArrayBuffer(); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024, arrayPool: mock.Object)) + { + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + } + mock.Verify(m => m.Return(It.IsAny(), true), Times.Exactly(1)); + } + + [Fact] + public void Dispose_Read_ObjectDisposedException() + { + var json = Encoding.UTF8.GetBytes(SmallJson); + Assert.Throws(() => + { + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Dispose(); + reader.Read(); + } + }); + } + + [Fact] + public void Dispose_Skip_ObjectDisposedException() + { + var json = Encoding.UTF8.GetBytes(SmallJson); + Assert.Throws(() => + { + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Dispose(); + reader.Skip(); + } + }); + } + + [Theory] + [InlineData("{\"object1\": { \"a\":\"asdad\" }")] + [InlineData("{\"object1\": { \"a\":\"asdad }}")] + [InlineData("{\"object1\": \"a\":\"asdad\" }}")] + public void Skip_WhenReadingWithMalformedJson(string malformedJson) + { + var json = Encoding.UTF8.GetBytes(malformedJson); + + Assert.ThrowsAny(() => + { + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Skip(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + } + }); + } + + [Fact] + public void Skip_WhenReadingWithoutOverflow_SkipObject() + { + var json = Encoding.UTF8.GetBytes(JsonWithoutOverflow); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + reader.Skip(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + } + } + + [Fact] + public void Skip_WhenReadingWithOverflow_Skip() + { + var json = Encoding.UTF8.GetBytes(JsonWithOverflow); + var mock = SetupMockArrayBuffer(); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024, mock.Object)) + { + reader.Read(); + reader.Skip(); + reader.Read(); + reader.Skip(); + Assert.Equal(JsonTokenType.EndObject, reader.TokenType); + reader.Read(); + Assert.Equal("object3", reader.GetString()); + mock.Verify(m => m.Rent(1024), Times.Exactly(1)); + } + } + + [Fact] + public void Skip_WhenReadingWithOverflowObject_ResizeBuffer() + { + var json = Encoding.UTF8.GetBytes(JsonWithOverflowObject); + var mock = SetupMockArrayBuffer(); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream, 1024, mock.Object)) + { + reader.Read(); + reader.Skip(); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + Assert.Equal("object2", reader.GetString()); + mock.Verify(m => m.Rent(1024), Times.Exactly(1)); + mock.Verify(m => m.Rent(2048), Times.Exactly(1)); + mock.Verify(m => m.Return(It.IsAny(), true), Times.Exactly(1)); + } + } + + [Fact] + public void ReadNextTokenAsString_WhenCalled_AdvanceToken() + { + var json = Encoding.UTF8.GetBytes("{\"token\":\"value\"}"); + + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + var result = reader.ReadNextTokenAsString(); + Assert.Equal(JsonTokenType.String, reader.TokenType); + Assert.Equal("value", result); + } + } + + [Fact] + public void ReadNextTokenAsString_WithMalformedJson_GetException() + { + var json = Encoding.UTF8.GetBytes("{\"token\":\"value}"); + Assert.ThrowsAny(() => + { + using (var stream = new MemoryStream(json)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + reader.ReadNextTokenAsString(); + } + }); + } + + [Theory] + [InlineData("true", JsonTokenType.True)] + [InlineData("false", JsonTokenType.False)] + [InlineData("-2", JsonTokenType.Number)] + [InlineData("3.14", JsonTokenType.Number)] + [InlineData("{}", JsonTokenType.StartObject)] + [InlineData("[]", JsonTokenType.StartArray)] + [InlineData("[true]", JsonTokenType.StartArray)] + [InlineData("[-2]", JsonTokenType.StartArray)] + [InlineData("[3.14]", JsonTokenType.StartArray)] + [InlineData("[\"a\", \"b\"]", JsonTokenType.StartArray)] + public void ReadDelimitedString_WhenValueIsNotString_Throws(string value, JsonTokenType expectedTokenType) + { + var json = $"{{\"a\":{value}}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + var tokenType = JsonTokenType.None; + var exceptionThrown = Assert.Throws(() => + { + using var stream = new MemoryStream(encodedBytes); + using var reader = new Utf8JsonStreamReader(stream); + reader.Read(); + try + { + reader.ReadDelimitedString(); + } + finally + { + tokenType = reader.TokenType; + } + }); + Assert.NotNull(exceptionThrown.InnerException); + Assert.IsType(typeof(InvalidCastException), exceptionThrown.InnerException); + Assert.Equal(expectedTokenType, tokenType); + } + + [Fact] + public void ReadDelimitedString_WhenValueIsString_ReturnsValue() + { + const string expectedResult = "b"; + var json = $"{{\"a\":\"{expectedResult}\"}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + IEnumerable actualResults = reader.ReadDelimitedString(); + Assert.Collection(actualResults, actualResult => Assert.Equal(expectedResult, actualResult)); + Assert.Equal(JsonTokenType.String, reader.TokenType); + } + } + + [Theory] + [InlineData("b,c,d")] + [InlineData("b c d")] + public void ReadDelimitedString_WhenValueIsDelimitedString_ReturnsValues(string value) + { + string[] expectedResults = value.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); + var json = $"{{\"a\":\"{value}\"}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + IEnumerable actualResults = reader.ReadDelimitedString(); + Assert.Equal(expectedResults, actualResults); + Assert.Equal(JsonTokenType.String, reader.TokenType); + } + } + + [Theory] + [InlineData("null")] + [InlineData("\"b\"")] + [InlineData("{}")] + public void ReadStringArrayAsIList_WhenValueIsNotArray_ReturnsNull(string value) + { + var json = $"{{\"a\":{value}}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + reader.Read(); + Assert.NotEqual(JsonTokenType.PropertyName, reader.TokenType); + IList actualValues = reader.ReadStringArrayAsIList(); + Assert.Null(actualValues); + } + } + + [Fact] + public void ReadStringArrayAsIList_WhenValueIsEmptyArray_ReturnsNull() + { + var encodedBytes = Encoding.UTF8.GetBytes("{\"a\":[]}"); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + reader.Read(); + Assert.NotEqual(JsonTokenType.PropertyName, reader.TokenType); + IList actualValues = reader.ReadStringArrayAsIList(); + Assert.Null(actualValues); + } + } + + [Fact] + public void ReadStringArrayAsIList_WithSupportedTypes_ReturnsStringArray() + { + var encodedBytes = Encoding.UTF8.GetBytes("[\"a\",-2,3.14,true,null]"); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + IList actualValues = reader.ReadStringArrayAsIList(); + + Assert.Collection( + actualValues, + actualValue => Assert.Equal("a", actualValue), + actualValue => Assert.Equal("-2", actualValue), + actualValue => Assert.Equal("3.14", actualValue), + actualValue => Assert.Equal("True", actualValue), + actualValue => Assert.Equal(null, actualValue)); + Assert.Equal(JsonTokenType.EndArray, reader.TokenType); + } + } + + [Theory] + [InlineData("[]")] + [InlineData("{}")] + public void ReadStringArrayAsIList_WithUnsupportedTypes_Throws(string element) + { + var encodedBytes = Encoding.UTF8.GetBytes($"[{element}]"); + Assert.Throws(() => + { + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.ReadStringArrayAsIList(); + } + }); + } + + + [Theory] + [InlineData("true", true)] + [InlineData("false", false)] + public void ReadNextTokenAsBoolOrFalse_WithValidValues_ReturnsBoolean(string value, bool expectedResult) + { + var json = $"{{\"a\":{value}}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + bool actualResult = reader.ReadNextTokenAsBoolOrFalse(); + Assert.Equal(expectedResult, actualResult); + } + } + + [Theory] + [InlineData("\"words\"")] + [InlineData("-3")] + [InlineData("3.3")] + [InlineData("[]")] + [InlineData("{}")] + public void ReadNextTokenAsBoolOrFalse_WithInvalidValues_ReturnsFalse(string value) + { + var json = $"{{\"a\":{value}}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + bool actualResult = reader.ReadNextTokenAsBoolOrFalse(); + Assert.False(actualResult); + } + } + + [Fact] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsNull_ReturnsNull() + { + const string json = "{\"a\":null}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + IEnumerable actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + Assert.Null(actualResults); + Assert.Equal(JsonTokenType.Null, reader.TokenType); + } + } + + [Theory] + [InlineData("true", JsonTokenType.True)] + [InlineData("false", JsonTokenType.False)] + [InlineData("-2", JsonTokenType.Number)] + [InlineData("3.14", JsonTokenType.Number)] + [InlineData("{}", JsonTokenType.StartObject)] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsNotString_ReturnsNull( + string value, + JsonTokenType expectedTokenType) + { + var json = $"{{\"a\":{value}}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + + IEnumerable actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + + Assert.Null(actualResults); + Assert.Equal(expectedTokenType, reader.TokenType); + } + } + + [Fact] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsString_ReturnsValue() + { + const string expectedResult = "b"; + var json = $"{{\"a\":\"{expectedResult}\"}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + + IEnumerable actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + + Assert.Collection(actualResults, actualResult => Assert.Equal(expectedResult, actualResult)); + Assert.Equal(JsonTokenType.String, reader.TokenType); + } + } + + [Theory] + [InlineData("b,c,d")] + [InlineData("b c d")] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsDelimitedString_ReturnsValue(string expectedResult) + { + var json = $"{{\"a\":\"{expectedResult}\"}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + + IEnumerable actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + + Assert.Collection(actualResults, actualResult => Assert.Equal(expectedResult, actualResult)); + Assert.Equal(JsonTokenType.String, reader.TokenType); + } + } + + [Fact] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsEmptyArray_ReturnsEmptyList() + { + const string json = "{\"a\":[]}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + + IReadOnlyList actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + + Assert.Empty(actualResults); + Assert.Equal(JsonTokenType.EndArray, reader.TokenType); + } + } + + [Theory] + [InlineData("null", null)] + [InlineData("true", "True")] + [InlineData("-2", "-2")] + [InlineData("3.14", "3.14")] + [InlineData("\"b\"", "b")] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsConvertibleToString_ReturnsValueAsString( + string value, + string expectedResult) + { + var json = $"{{\"a\":[{value}]}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + + IEnumerable actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + + Assert.Collection(actualResults, actualResult => Assert.Equal(expectedResult, actualResult)); + Assert.Equal(JsonTokenType.EndArray, reader.TokenType); + } + } + + [Theory] + [InlineData("[]", JsonTokenType.StartArray)] + [InlineData("{}", JsonTokenType.StartObject)] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsNotConvertibleToString_ReturnsValueAsString( + string value, + JsonTokenType expectedToken) + { + var json = $"{{\"a\":[{value}]}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + var tokenType = JsonTokenType.None; + var exceptionThrown = Assert.Throws(() => + { + using var stream = new MemoryStream(encodedBytes); + using var reader = new Utf8JsonStreamReader(stream); + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + try + { + reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + } + finally + { + tokenType = reader.TokenType; + } + }); + Assert.Equal(expectedToken, tokenType); + } + + [Fact] + public void ReadNextStringOrArrayOfStringsAsReadOnlyList_WhenValueIsArrayOfStrings_ReturnsValues() + { + string[] expectedResults = { "b", "c" }; + var json = $"{{\"a\":[{string.Join(",", expectedResults.Select(expectedResult => $"\"{expectedResult}\""))}]}}"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + reader.Read(); + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + + IEnumerable actualResults = reader.ReadNextStringOrArrayOfStringsAsReadOnlyList(); + + Assert.Equal(expectedResults, actualResults); + Assert.Equal(JsonTokenType.EndArray, reader.TokenType); + } + } + + [Fact] + public void ReadStringArrayAsReadOnlyListFromArrayStart_WhenValuesAreConvertibleToString_ReturnsReadOnlyList() + { + const string json = "[null, true, -2, 3.14, \"a\"]"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + using (var stream = new MemoryStream(encodedBytes)) + using (var reader = new Utf8JsonStreamReader(stream)) + { + Assert.Equal(JsonTokenType.StartArray, reader.TokenType); + + IEnumerable actualResults = reader.ReadStringArrayAsReadOnlyListFromArrayStart(); + + Assert.Collection( + actualResults, + actualResult => Assert.Equal(null, actualResult), + actualResult => Assert.Equal("True", actualResult), + actualResult => Assert.Equal("-2", actualResult), + actualResult => Assert.Equal("3.14", actualResult), + actualResult => Assert.Equal("a", actualResult)); + Assert.Equal(JsonTokenType.EndArray, reader.TokenType); + } + } + + [Theory] + [InlineData("[]", JsonTokenType.StartArray)] + [InlineData("{}", JsonTokenType.StartObject)] + public void ReadStringArrayAsReadOnlyListFromArrayStart_WhenValuesAreNotConvertibleToString_Throws( + string value, + JsonTokenType expectedToken) + { + var json = $"[{value}]"; + var encodedBytes = Encoding.UTF8.GetBytes(json); + var tokenType = JsonTokenType.None; + var exceptionThrown = Assert.Throws(() => + { + using var stream = new MemoryStream(encodedBytes); + using var reader = new Utf8JsonStreamReader(stream); + Assert.Equal(JsonTokenType.StartArray, reader.TokenType); + try + { + reader.ReadStringArrayAsReadOnlyListFromArrayStart(); + } + finally + { + tokenType = reader.TokenType; + } + }); + Assert.Equal(expectedToken, tokenType); + } + + private Mock> SetupMockArrayBuffer() + { + Mock> mock = new Mock>(); + mock.Setup(m => m.Rent(1024)).Returns(new byte[1024]); + mock.Setup(m => m.Rent(2048)).Returns(new byte[2048]); + mock.Setup(m => m.Return(It.IsAny(), It.IsAny())); + + return mock; + } + } +}